<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://blog.kuizuo.me/blog</id>
    <title>愧怍</title>
    <updated>2026-03-22T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://blog.kuizuo.me/blog"/>
    <subtitle>feedId:41215011978385457+userId:41840354283324416</subtitle>
    <icon>https://blog.kuizuo.me/img/favicon.ico</icon>
    <rights>Copyright © 2026 愧怍 Built with Docusaurus.&lt;p&gt;&lt;a href="http://beian.miit.gov.cn/" class="footer_lin"&gt;闽ICP备2020017848号-3&lt;/a&gt;&lt;/p&gt;</rights>
    <entry>
        <title type="html"><![CDATA[Workspace 是什么？一篇讲明白 Monorepo 里为什么要用它]]></title>
        <id>https://blog.kuizuo.me/blog/workspace-introduction</id>
        <link href="https://blog.kuizuo.me/blog/workspace-introduction"/>
        <updated>2026-03-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[用通俗的方式讲清什么是 Workspace、它解决了什么问题，以及在 pnpm Monorepo 里它是如何让多个包互相引用的。]]></summary>
        <content type="html"><![CDATA[<p>很多人第一次接触 <code>pnpm workspace</code>、<code>yarn workspace</code> 或 <code>monorepo</code>，都会有点懵：</p>
<p><strong>为什么一个仓库里会有这么多包？它们又是怎么互相引用的？</strong></p>
<p>这时候你大概率会看到一个词：<code>Workspace</code>。</p>
<p>如果只用一句人话解释它，可以这么理解：</p>
<p><strong>Workspace 就是把一个仓库里的多个包组织起来，让它们既能一起管理，又能直接互相依赖。</strong></p>
<p>它最常见的使用场景，就是 <code>Monorepo</code>。</p>
<p>比如一个前端仓库里，可能同时有：</p>
<ul>
<li>一个主应用</li>
<li>一个组件库</li>
<li>一个工具包</li>
<li>一个命令行脚手架</li>
</ul>
<p>这些包都放在同一个仓库里，但又不是一坨代码混在一起。<br>
<!-- -->这时候，Workspace 就是把它们串起来的那套机制。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一先说结论workspace-到底解决了什么问题">一、先说结论：Workspace 到底解决了什么问题？<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E4%B8%80%E5%85%88%E8%AF%B4%E7%BB%93%E8%AE%BAworkspace-%E5%88%B0%E5%BA%95%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>你可以先记住这句话：</p>
<p><strong>没有 Workspace，多包之间协作很麻烦；有了 Workspace，同仓库里的包可以像“本地模块”一样直接互相使用。</strong></p>
<p>它主要解决的是这几个问题：</p>
<ul>
<li>一个仓库里如何管理多个包</li>
<li>包与包之间如何直接引用</li>
<li>多个包的依赖如何统一安装</li>
<li>多个包的脚本如何统一执行</li>
</ul>
<p>所以 Workspace 本质上不是“某个框架功能”，而是一种<strong>多包协作机制</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二为什么会需要-workspace">二、为什么会需要 Workspace？<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E4%BA%8C%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E9%9C%80%E8%A6%81-workspace" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>先假设一个很常见的场景。</p>
<p>你有两个项目：</p>
<ul>
<li><code>cli-plugin-publish</code></li>
<li><code>cli-devkit</code></li>
</ul>
<p>其中 <code>cli-plugin-publish</code> 依赖 <code>cli-devkit</code>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-没有-workspace-的时候">1. 没有 Workspace 的时候<a href="https://blog.kuizuo.me/blog/workspace-introduction#1-%E6%B2%A1%E6%9C%89-workspace-%E7%9A%84%E6%97%B6%E5%80%99" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>如果这两个包是彼此独立的，那么开发流程通常会很别扭：</p>
<ol>
<li>先开发 <code>cli-devkit</code></li>
<li>把它发布到 npm</li>
<li>再回到 <code>cli-plugin-publish</code> 里安装这个新版本</li>
<li>如果 <code>cli-devkit</code> 改了，还得重新发布、重新安装</li>
</ol>
<p>也就是说，你明明只是想在本地调试两个关联模块，却不得不走一遍“发布 -&gt; 安装 -&gt; 再测试”的流程。</p>
<p>这会带来几个问题：</p>
<ul>
<li>开发效率低</li>
<li>调试成本高</li>
<li>版本切换麻烦</li>
<li>很容易出现“我本地改了，但另一个包还没用上”的情况</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-有-workspace-的时候">2. 有 Workspace 的时候<a href="https://blog.kuizuo.me/blog/workspace-introduction#2-%E6%9C%89-workspace-%E7%9A%84%E6%97%B6%E5%80%99" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>有了 Workspace 后，思路就完全不一样了：</p>
<p><strong>既然这些包都在同一个仓库里，那就直接让它们在本地互相引用。</strong></p>
<p>也就是说：</p>
<ul>
<li>不需要先发 npm</li>
<li>不需要来回安装</li>
<li>一个包改完，另一个包可以马上感知到</li>
</ul>
<p>这就是 Workspace 最核心的价值。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三workspace-最简单的人话定义">三、Workspace 最简单的人话定义<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E4%B8%89workspace-%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84%E4%BA%BA%E8%AF%9D%E5%AE%9A%E4%B9%89" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>你可以把 Workspace 想成一个“包清单 + 协作规则”。</p>
<p>它主要做两件事：</p>
<ol>
<li>告诉包管理器：这个仓库里，哪些目录算是包</li>
<li>告诉包管理器：这些包之间可以直接使用本地版本互相依赖</li>
</ol>
<p>所以它不是单纯“多建几个文件夹”，而是让包管理器真正理解：</p>
<p><strong>这个仓库不是一个项目，而是一组有关联的项目。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四在-pnpm-里workspace-是怎么声明的">四、在 pnpm 里，Workspace 是怎么声明的？<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E5%9B%9B%E5%9C%A8-pnpm-%E9%87%8Cworkspace-%E6%98%AF%E6%80%8E%E4%B9%88%E5%A3%B0%E6%98%8E%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>在 <code>pnpm</code> 里，最常见的方式是根目录放一个 <code>pnpm-workspace.yaml</code>。</p>
<p>例如：</p>
<div class="language-yaml codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-yaml codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token key atrule" style="color:hsl(35, 99%, 36%)">packages</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">-</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"apps/*"</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">-</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"packages/*"</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>它表达的意思很简单：</p>
<ul>
<li><code>apps</code> 目录下匹配到的子目录，都是一个包</li>
<li><code>packages</code> 目录下匹配到的子目录，也都是一个包</li>
</ul>
<p>比如这几个目录，pnpm 就会把它们识别成 workspace 里的成员：</p>
<ul>
<li><code>apps/workbench</code></li>
<li><code>packages/cli-plugin-publish</code></li>
<li><code>packages/cli-devkit</code></li>
</ul>
<p>也就是说，从这一刻开始，pnpm 不再只把它们看成普通文件夹，而是把它们当成一组能协作的包。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五包和包之间是怎么互相引用的">五、包和包之间是怎么互相引用的？<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E4%BA%94%E5%8C%85%E5%92%8C%E5%8C%85%E4%B9%8B%E9%97%B4%E6%98%AF%E6%80%8E%E4%B9%88%E4%BA%92%E7%9B%B8%E5%BC%95%E7%94%A8%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这也是很多人最关心的部分。</p>
<p>假设 <code>cli-plugin-publish</code> 依赖本地的 <code>cli-devkit</code>，在 <code>package.json</code> 里可能会这么写：</p>
<div class="language-json codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-json codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token property" style="color:hsl(5, 74%, 59%)">"dependencies"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token property" style="color:hsl(5, 74%, 59%)">"@osl/cli-devkit"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"workspace:*"</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token property" style="color:hsl(5, 74%, 59%)">"inquirer"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"^8.2.6"</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这里最关键的是：</p>
<div class="language-json codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-json codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token property" style="color:hsl(5, 74%, 59%)">"@osl/cli-devkit"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"workspace:*"</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p><code>workspace:*</code> 的意思可以简单理解成：</p>
<p><strong>优先使用当前 Workspace 里那个本地包，而不是去 npm 上下载一个远程版本。</strong></p>
<p>所以它带来的效果是：</p>
<ul>
<li>直接使用本地 <code>packages/cli-devkit</code></li>
<li>不需要先发布到 npm</li>
<li>修改 <code>cli-devkit</code> 后，<code>cli-plugin-publish</code> 很快就能用到新代码</li>
</ul>
<p>如果你以前的认知是“依赖必须先发包才能给别人用”，那 Workspace 就是在告诉你：</p>
<p><strong>同一个仓库里的包，不需要绕远路走 npm，可以直接本地联动。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六统一管理依赖这件事为什么很重要">六、统一管理依赖，这件事为什么很重要？<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E5%85%AD%E7%BB%9F%E4%B8%80%E7%AE%A1%E7%90%86%E4%BE%9D%E8%B5%96%E8%BF%99%E4%BB%B6%E4%BA%8B%E4%B8%BA%E4%BB%80%E4%B9%88%E5%BE%88%E9%87%8D%E8%A6%81" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>使用 Workspace 后，一个很明显的变化是：</p>
<p><strong>你通常只需要在仓库根目录执行一次安装命令。</strong></p>
<p>比如：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">install</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这条命令会做的事，不只是“安装当前目录依赖”，而是：</p>
<ul>
<li>安装整个 workspace 里所有包的依赖</li>
<li>建立它们之间的本地链接关系</li>
<li>尽量复用依赖，减少重复安装</li>
</ul>
<p>这就带来两个实际好处：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-依赖安装更统一">1. 依赖安装更统一<a href="https://blog.kuizuo.me/blog/workspace-introduction#1-%E4%BE%9D%E8%B5%96%E5%AE%89%E8%A3%85%E6%9B%B4%E7%BB%9F%E4%B8%80" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>你不需要每进一个包都手动跑一遍安装命令。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-依赖复用更充分">2. 依赖复用更充分<a href="https://blog.kuizuo.me/blog/workspace-introduction#2-%E4%BE%9D%E8%B5%96%E5%A4%8D%E7%94%A8%E6%9B%B4%E5%85%85%E5%88%86" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>多个包用到相同依赖时，pnpm 可以更高效地复用，节省空间。</p>
<p>当然，这里还有一个重要前提：</p>
<p><strong>虽然依赖可以统一安装，但每个包依然只能访问自己声明过的依赖。</strong></p>
<p>这也是 pnpm 在 workspace 场景里依然保持“严格依赖”的原因。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七没有-workspace-和有-workspace到底差在哪">七、没有 Workspace 和有 Workspace，到底差在哪？<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E4%B8%83%E6%B2%A1%E6%9C%89-workspace-%E5%92%8C%E6%9C%89-workspace%E5%88%B0%E5%BA%95%E5%B7%AE%E5%9C%A8%E5%93%AA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>拿刚才那个例子继续看，会更直观。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="没有-workspace">没有 Workspace<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E6%B2%A1%E6%9C%89-workspace" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">项目 A（cli-plugin-publish）</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── 依赖项目 B（cli-devkit）</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ├── 必须先把 B 发布到 npm</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ├── 然后 A 再安装 B</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    └── B 改了以后，还要重新发布再安装</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这种方式的问题是，明明只是本地两个项目协作，却被迫走成了“线上发包流程”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="有-workspace">有 Workspace<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E6%9C%89-workspace" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">fe-universal-repo</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">├── packages/cli-devkit</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── packages/cli-plugin-publish</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    └── 直接依赖 "@osl/cli-devkit": "workspace:*"</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这时候流程就变成：</p>
<ul>
<li><code>cli-devkit</code> 改完</li>
<li><code>cli-plugin-publish</code> 可以直接使用</li>
<li>不需要发布</li>
<li>不需要额外安装远程版本</li>
</ul>
<p>从开发体验上讲，这几乎就是“本地联调”的升级版。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八workspace-的几个核心好处">八、Workspace 的几个核心好处<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E5%85%ABworkspace-%E7%9A%84%E5%87%A0%E4%B8%AA%E6%A0%B8%E5%BF%83%E5%A5%BD%E5%A4%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-开发效率更高">1. 开发效率更高<a href="https://blog.kuizuo.me/blog/workspace-introduction#1-%E5%BC%80%E5%8F%91%E6%95%88%E7%8E%87%E6%9B%B4%E9%AB%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这是最直观的收益。</p>
<p>包和包之间的联动不需要经过发布流程，本地修改后很快就能验证。</p>
<p>特别适合这些场景：</p>
<ul>
<li>一个应用依赖多个内部包</li>
<li>一个组件库被多个项目共同使用</li>
<li>一个工具包和多个插件一起开发</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-依赖管理更统一">2. 依赖管理更统一<a href="https://blog.kuizuo.me/blog/workspace-introduction#2-%E4%BE%9D%E8%B5%96%E7%AE%A1%E7%90%86%E6%9B%B4%E7%BB%9F%E4%B8%80" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>多个包都放在同一个仓库里，依赖安装、版本调整、脚本执行都会更集中。</p>
<p>这比“每个项目各自一套”更好维护。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-节省空间">3. 节省空间<a href="https://blog.kuizuo.me/blog/workspace-introduction#3-%E8%8A%82%E7%9C%81%E7%A9%BA%E9%97%B4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>多个包共享依赖安装体系，重复依赖可以更好地复用。</p>
<p>这在 <code>pnpm</code> 里会特别明显，因为它本身就很强调依赖复用。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-版本管理更简单">4. 版本管理更简单<a href="https://blog.kuizuo.me/blog/workspace-introduction#4-%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86%E6%9B%B4%E7%AE%80%E5%8D%95" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>多个包在同一个仓库里，统一改版本、统一发布、统一维护会方便很多。</p>
<p>对于组件库、工具链、脚手架这类项目尤其明显。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-原子性提交更好">5. 原子性提交更好<a href="https://blog.kuizuo.me/blog/workspace-introduction#5-%E5%8E%9F%E5%AD%90%E6%80%A7%E6%8F%90%E4%BA%A4%E6%9B%B4%E5%A5%BD" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>假设你同时修改了：</p>
<ul>
<li><code>cli-devkit</code></li>
<li><code>cli-plugin-publish</code></li>
</ul>
<p>那你完全可以在一个提交里把这两部分改动一起提交。</p>
<p>这样好处是：</p>
<ul>
<li>改动关系清楚</li>
<li>依赖关系一致</li>
<li>不容易出现“这个包改了，但另一个包还没跟上”的问题</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="九workspace-适合哪些项目">九、Workspace 适合哪些项目？<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E4%B9%9Dworkspace-%E9%80%82%E5%90%88%E5%93%AA%E4%BA%9B%E9%A1%B9%E7%9B%AE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>不是所有项目都一定要用 Workspace，但下面这些场景通常很适合：</p>
<ul>
<li>一个仓库里有多个应用</li>
<li>一个仓库里有多个共享包</li>
<li>组件库 + 文档站 + 示例项目放在一起</li>
<li>工具链项目拆成多个子包维护</li>
<li>Monorepo 项目</li>
</ul>
<p>如果你的仓库里只有一个单独项目，没有本地多包协作需求，那 Workspace 的必要性就没那么强。</p>
<p>所以它不是“高级就一定要用”，而是：</p>
<p><strong>当你开始管理多个相互依赖的包时，Workspace 会非常顺手。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十几个常用命令">十、几个常用命令<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E5%8D%81%E5%87%A0%E4%B8%AA%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>在实际使用里，下面几个命令最常见。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="安装整个-workspace-的依赖">安装整个 Workspace 的依赖<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E5%AE%89%E8%A3%85%E6%95%B4%E4%B8%AA-workspace-%E7%9A%84%E4%BE%9D%E8%B5%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">install</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="只给某个包安装依赖">只给某个包安装依赖<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E5%8F%AA%E7%BB%99%E6%9F%90%E4%B8%AA%E5%8C%85%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--filter</span><span class="token plain"> @osl/tiny-plugin-publish </span><span class="token function" style="color:hsl(221, 87%, 60%)">add</span><span class="token plain"> lodash</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="在某个包里执行脚本">在某个包里执行脚本<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E5%9C%A8%E6%9F%90%E4%B8%AA%E5%8C%85%E9%87%8C%E6%89%A7%E8%A1%8C%E8%84%9A%E6%9C%AC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--filter</span><span class="token plain"> @osl/tiny-plugin-publish build</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="给所有包递归执行脚本">给所有包递归执行脚本<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E7%BB%99%E6%89%80%E6%9C%89%E5%8C%85%E9%80%92%E5%BD%92%E6%89%A7%E8%A1%8C%E8%84%9A%E6%9C%AC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-r</span><span class="token plain"> build</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这里的 <code>-r</code> 是 <code>recursive</code>，意思是递归地对 workspace 中的多个包执行命令。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十一最后怎么记住-workspace">十一、最后怎么记住 Workspace？<a href="https://blog.kuizuo.me/blog/workspace-introduction#%E5%8D%81%E4%B8%80%E6%9C%80%E5%90%8E%E6%80%8E%E4%B9%88%E8%AE%B0%E4%BD%8F-workspace" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果要把 Workspace 压缩成一句最容易记住的话，那就是：</p>
<p><strong>Workspace = 把多个包放进同一个仓库里统一管理，并允许它们直接使用彼此的本地代码。</strong></p>
<p>所以它真正解决的，不只是“目录怎么放”，而是：</p>
<ul>
<li>多包如何协作</li>
<li>本地包如何互相依赖</li>
<li>多个包如何统一安装和执行脚本</li>
</ul>
<p>如果你把它想成“Monorepo 里的本地包协作机制”，基本就不会理解偏。</p>
<p>对于前端工程化来说，Workspace 几乎就是从“单项目开发”走向“多包协作开发”的第一步。</p>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="Workspace" term="Workspace"/>
        <category label="pnpm" term="pnpm"/>
        <category label="Monorepo" term="Monorepo"/>
        <category label="前端工程化" term="前端工程化"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Changesets 是什么？一篇讲明白 Monorepo 里的版本管理和发版流程]]></title>
        <id>https://blog.kuizuo.me/blog/changesets-introduction</id>
        <link href="https://blog.kuizuo.me/blog/changesets-introduction"/>
        <updated>2026-03-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[用通俗的方式讲清 Changesets 是什么、为什么 Monorepo 需要它，以及它如何管理版本号、生成 CHANGELOG 并协助发版。]]></summary>
        <content type="html"><![CDATA[<p>当项目开始走向 <code>Monorepo</code>，一个仓库里同时维护多个包时，很快就会遇到一个现实问题：</p>
<p><strong>代码能一起写了，那版本号、CHANGELOG、发版流程怎么一起管？</strong></p>
<p>这时候很多团队会用到一个工具：<code>Changesets</code>。</p>
<p>如果用一句最通俗的话解释它，可以这么理解：</p>
<p><strong>Changesets 就像 Monorepo 里的“发版记录单”系统。</strong></p>
<p>你每做完一次改动，就先记一张“这次改了什么、该升哪个版本”的单子；等准备发版时，再统一计算版本号、生成 CHANGELOG、执行发布。</p>
<p>它最适合的场景，就是：</p>
<ul>
<li>一个仓库里有多个 npm 包</li>
<li>包和包之间有依赖关系</li>
<li>团队希望版本管理更清晰、发布流程更自动化</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一先说结论changesets-到底解决了什么问题">一、先说结论：Changesets 到底解决了什么问题？<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E4%B8%80%E5%85%88%E8%AF%B4%E7%BB%93%E8%AE%BAchangesets-%E5%88%B0%E5%BA%95%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果项目里只有一个包，手动改版本号还不算特别痛苦。</p>
<p>但一旦到了 Monorepo，情况会立刻复杂很多。</p>
<p>比如你的仓库里有这些包：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">packages/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">├── cli-devkit</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">├── cli-plugin-publish</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── cli-plugin-changeset</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>而且：</p>
<ul>
<li><code>cli-plugin-publish</code> 依赖 <code>cli-devkit</code></li>
<li><code>cli-plugin-changeset</code> 也依赖 <code>cli-devkit</code></li>
</ul>
<p>这时候你会遇到几个典型问题：</p>
<ul>
<li>哪个包这次需要发版？</li>
<li>它应该升 <code>patch</code>、<code>minor</code> 还是 <code>major</code>？</li>
<li>如果底层包升级了，依赖它的其他包要不要一起变？</li>
<li>CHANGELOG 谁来写？</li>
<li>多个人同时改包时，怎么避免版本管理混乱？</li>
</ul>
<p>Changesets 就是专门解决这些问题的。</p>
<p>它最核心的能力有四个：</p>
<ul>
<li>管理多个包的版本号</li>
<li>自动生成 CHANGELOG</li>
<li>协调内部依赖包的版本更新</li>
<li>协助完成发版流程</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二为什么-monorepo-特别需要-changesets">二、为什么 Monorepo 特别需要 Changesets？<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E4%BA%8C%E4%B8%BA%E4%BB%80%E4%B9%88-monorepo-%E7%89%B9%E5%88%AB%E9%9C%80%E8%A6%81-changesets" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>可以先想象一下，如果<strong>没有</strong> Changesets，会发生什么。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-手动管理版本号很容易乱">1. 手动管理版本号很容易乱<a href="https://blog.kuizuo.me/blog/changesets-introduction#1-%E6%89%8B%E5%8A%A8%E7%AE%A1%E7%90%86%E7%89%88%E6%9C%AC%E5%8F%B7%E5%BE%88%E5%AE%B9%E6%98%93%E4%B9%B1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>你改了一个包，要自己判断：</p>
<ul>
<li>这是 <code>patch</code> 还是 <code>minor</code>？</li>
<li>版本该从 <code>1.1.19</code> 改到多少？</li>
<li>改的是底层包，依赖它的上层包要不要一起动？</li>
</ul>
<p>如果包只有一个，也许还能靠人脑记住。<br>
<!-- -->但当仓库里有 5 个、10 个、20 个包时，这件事就很容易失控。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-changelog-很容易没人维护">2. CHANGELOG 很容易没人维护<a href="https://blog.kuizuo.me/blog/changesets-introduction#2-changelog-%E5%BE%88%E5%AE%B9%E6%98%93%E6%B2%A1%E4%BA%BA%E7%BB%B4%E6%8A%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>理论上每次发版都应该有变更记录。<br>
<!-- -->但现实是，很多团队最后都会变成：</p>
<ul>
<li>有人忘了写</li>
<li>有人随便写一句</li>
<li>有人直接不写</li>
</ul>
<p>结果就是，版本发了，但过了几周大家都不记得这版到底改了什么。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-包之间有依赖关系版本不是独立变化的">3. 包之间有依赖关系，版本不是独立变化的<a href="https://blog.kuizuo.me/blog/changesets-introduction#3-%E5%8C%85%E4%B9%8B%E9%97%B4%E6%9C%89%E4%BE%9D%E8%B5%96%E5%85%B3%E7%B3%BB%E7%89%88%E6%9C%AC%E4%B8%8D%E6%98%AF%E7%8B%AC%E7%AB%8B%E5%8F%98%E5%8C%96%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>例如：</p>
<ul>
<li><code>cli-devkit</code> 升级了</li>
<li><code>cli-plugin-publish</code> 依赖它</li>
<li><code>cli-plugin-changeset</code> 也依赖它</li>
</ul>
<p>那你就不能只盯着一个包看，而要考虑整条依赖链。</p>
<p>这也是 Monorepo 发版麻烦的地方：</p>
<p><strong>问题不是“改哪个包版本”，而是“哪些包会被这次改动影响”。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三changesets-的核心思路其实很简单">三、Changesets 的核心思路其实很简单<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E4%B8%89changesets-%E7%9A%84%E6%A0%B8%E5%BF%83%E6%80%9D%E8%B7%AF%E5%85%B6%E5%AE%9E%E5%BE%88%E7%AE%80%E5%8D%95" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>很多人第一次接触 Changesets，会觉得它像一个复杂的发版工具。<br>
<!-- -->其实它的思路非常朴素：</p>
<p><strong>先记录变更，再统一处理版本。</strong></p>
<p>也就是说，它不是要求你每改一次代码就立刻发版，而是分成两步：</p>
<ol>
<li>平时开发时，先记录“这次改动将来要怎么发”</li>
<li>真正发版时，再统一计算版本、生成 CHANGELOG、执行发布</li>
</ol>
<p>这也是它为什么特别适合团队协作。</p>
<p>因为每个开发者只需要对自己的改动负责，先写好“这次改了什么”；<br>
<!-- -->至于最后怎么批量升级版本，交给 Changesets 统一处理。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四changesets-是怎么工作的">四、Changesets 是怎么工作的？<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E5%9B%9Bchangesets-%E6%98%AF%E6%80%8E%E4%B9%88%E5%B7%A5%E4%BD%9C%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>可以把它理解成 3 个阶段：</p>
<ol>
<li>记录变更</li>
<li>计算版本</li>
<li>执行发布</li>
</ol>
<p>下面逐个看。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-第一步记录变更">1. 第一步：记录变更<a href="https://blog.kuizuo.me/blog/changesets-introduction#1-%E7%AC%AC%E4%B8%80%E6%AD%A5%E8%AE%B0%E5%BD%95%E5%8F%98%E6%9B%B4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>每次你完成一项功能或修复一个问题后，可以创建一个 changeset 文件：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">npx @osl/cli changeset </span><span class="token function" style="color:hsl(221, 87%, 60%)">add</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>执行之后，它通常会生成一个文件，比如：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">.changeset/cool-cats.md</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>内容可能长这样：</p>
<div class="language-md codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-md codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token front-matter-block punctuation" style="color:hsl(119, 34%, 47%)">---</span><span class="token front-matter-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token front-matter-block"></span><span class="token front-matter-block front-matter yaml language-yaml key atrule" style="color:hsl(35, 99%, 36%)">"@osl/tiny-plugin-publish"</span><span class="token front-matter-block front-matter yaml language-yaml punctuation" style="color:hsl(119, 34%, 47%)">:</span><span class="token front-matter-block front-matter yaml language-yaml"> patch</span><span class="token front-matter-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token front-matter-block"></span><span class="token front-matter-block punctuation" style="color:hsl(119, 34%, 47%)">---</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">修复了飞书文档标题包含分支名的问题</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这个文件其实就是一张“发版记录单”，记录了三件事：</p>
<ul>
<li>哪个包要更新：<code>@osl/tiny-plugin-publish</code></li>
<li>更新类型：<code>patch</code></li>
<li>这次改动的描述：修复了什么问题</li>
</ul>
<p>所以 changeset 文件本质上不是代码，而是：</p>
<p><strong>一次改动对应的一份发布说明。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-第二步统一计算版本">2. 第二步：统一计算版本<a href="https://blog.kuizuo.me/blog/changesets-introduction#2-%E7%AC%AC%E4%BA%8C%E6%AD%A5%E7%BB%9F%E4%B8%80%E8%AE%A1%E7%AE%97%E7%89%88%E6%9C%AC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>等到准备发版时，再执行：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">npx @osl/cli changeset version</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这一步会做很多自动化工作：</p>
<ul>
<li>读取所有未消费的 changeset 文件</li>
<li>计算每个包的新版本号</li>
<li>更新各个包的 <code>package.json</code></li>
<li>生成或更新 <code>CHANGELOG.md</code></li>
<li>根据配置同步处理内部依赖版本</li>
<li>删除已经处理过的 changeset 文件</li>
</ul>
<p>版本号的计算遵循语义化版本（SemVer）：</p>
<ul>
<li><code>patch</code>：小修复，向后兼容，例如 <code>1.1.19 -&gt; 1.1.20</code></li>
<li><code>minor</code>：新功能，向后兼容，例如 <code>1.1.19 -&gt; 1.2.0</code></li>
<li><code>major</code>：破坏性更新，不向后兼容，例如 <code>1.1.19 -&gt; 2.0.0</code></li>
</ul>
<p>也就是说，<code>version</code> 这一步不是“发包”，而是：</p>
<p><strong>把之前记录好的变更，统一变成真正的版本号和 CHANGELOG。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-第三步执行发布">3. 第三步：执行发布<a href="https://blog.kuizuo.me/blog/changesets-introduction#3-%E7%AC%AC%E4%B8%89%E6%AD%A5%E6%89%A7%E8%A1%8C%E5%8F%91%E5%B8%83" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>等版本和 changelog 都整理好之后，再进入发布阶段。</p>
<p>例如：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> publish:local</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token comment" style="color:hsl(230, 4%, 64%)"># 或</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> publish:prod</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>具体命令会根据你的项目脚本不同而不同，但通常这一阶段会完成：</p>
<ul>
<li>构建包</li>
<li>发布到 npm registry</li>
<li>创建 git tag</li>
<li>推送代码或发布结果</li>
</ul>
<p>所以如果要一句话概括整个流程，就是：</p>
<p><strong>changeset add 负责记账，changeset version 负责结算，publish 负责真正发出去。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五把它放进真实工作流里就很好理解了">五、把它放进真实工作流里，就很好理解了<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E4%BA%94%E6%8A%8A%E5%AE%83%E6%94%BE%E8%BF%9B%E7%9C%9F%E5%AE%9E%E5%B7%A5%E4%BD%9C%E6%B5%81%E9%87%8C%E5%B0%B1%E5%BE%88%E5%A5%BD%E7%90%86%E8%A7%A3%E4%BA%86" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>假设你修复了一个 bug，比如：</p>
<p><code>packages/cli-plugin-publish/src/lib/utils.ts</code></p>
<p>那么一套典型流程会是这样：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token comment" style="color:hsl(230, 4%, 64%)"># 1. 修改代码</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token comment" style="color:hsl(230, 4%, 64%)"># 2. 创建 changeset，记录这次改动</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">npx @osl/cli changeset </span><span class="token function" style="color:hsl(221, 87%, 60%)">add</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token comment" style="color:hsl(230, 4%, 64%)"># 3. 提交代码和 changeset 文件</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">git</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">add</span><span class="token plain"> </span><span class="token builtin class-name" style="color:hsl(35, 99%, 36%)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">git</span><span class="token plain"> commit </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-m</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"fix: 修复飞书文档标题"</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">git</span><span class="token plain"> push</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这时候通常还<strong>没有真正发版</strong>，只是把“将来要怎么发”记录下来了。</p>
<p>等真正准备发布时，再执行：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token comment" style="color:hsl(230, 4%, 64%)"># 4. 统一计算版本和 changelog</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">npx @osl/cli changeset version</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token comment" style="color:hsl(230, 4%, 64%)"># 5. 提交版本更新结果</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">git</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">add</span><span class="token plain"> </span><span class="token builtin class-name" style="color:hsl(35, 99%, 36%)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">git</span><span class="token plain"> commit </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-m</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"chore: version bump"</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">git</span><span class="token plain"> push</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token comment" style="color:hsl(230, 4%, 64%)"># 6. 正式发布</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> publish:local</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这个流程的好处是：</p>
<ul>
<li>平时开发时不用一边写代码一边手动改版本</li>
<li>多个改动可以先积累，等到合适时机统一发版</li>
<li>发版时不需要靠人肉统计“这次到底改了哪些包”</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六changesets-为什么特别适合团队协作">六、Changesets 为什么特别适合团队协作？<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E5%85%ADchangesets-%E4%B8%BA%E4%BB%80%E4%B9%88%E7%89%B9%E5%88%AB%E9%80%82%E5%90%88%E5%9B%A2%E9%98%9F%E5%8D%8F%E4%BD%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>因为它把“写代码”和“决定怎么发版”拆开了。</p>
<p>在团队协作里，每个开发者可以这样做：</p>
<ul>
<li>改自己的代码</li>
<li>给自己的改动补一个 changeset</li>
<li>把代码和 changeset 一起提交</li>
</ul>
<p>这样等分支合并后，仓库里就自然积累了一批“待发布记录”。</p>
<p>等到发版时，Changesets 会把这些记录统一汇总起来。</p>
<p>这就比传统的人肉流程稳定很多：</p>
<ul>
<li>不容易漏掉变更</li>
<li>不容易忘记写 CHANGELOG</li>
<li>不需要靠某个人临时整理版本影响范围</li>
</ul>
<p>所以你完全可以把 Changesets 理解成：</p>
<p><strong>团队协作下的发版中间层。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七内部依赖版本是怎么联动的">七、内部依赖版本是怎么联动的？<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E4%B8%83%E5%86%85%E9%83%A8%E4%BE%9D%E8%B5%96%E7%89%88%E6%9C%AC%E6%98%AF%E6%80%8E%E4%B9%88%E8%81%94%E5%8A%A8%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这是 Changesets 很有价值的一点。</p>
<p>比如你的配置里有这样一项：</p>
<div class="language-json codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-json codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token property" style="color:hsl(5, 74%, 59%)">"updateInternalDependencies"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"patch"</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>它的意思是：</p>
<p>如果某个内部依赖包升级了，那么依赖它的其他包也会按规则自动更新版本。</p>
<p>例如：</p>
<ul>
<li><code>cli-devkit</code> 从 <code>1.0.0</code> 升到 <code>1.1.0</code></li>
<li><code>cli-plugin-publish</code> 依赖 <code>cli-devkit</code></li>
</ul>
<p>那么在配置允许的情况下，<code>cli-plugin-publish</code> 也可能自动补一次版本更新，比如：</p>
<ul>
<li><code>1.1.19 -&gt; 1.1.20</code></li>
</ul>
<p>为什么要这样做？</p>
<p>因为在 Monorepo 里，发版不是孤立事件。<br>
<!-- -->底层包一变，依赖它的上层包往往也受影响。</p>
<p>Changesets 能做的，就是把这种“依赖链上的版本联动”也纳入自动化流程里。</p>
<p>这样会带来几个好处：</p>
<ul>
<li>保持依赖关系一致</li>
<li>减少版本冲突</li>
<li>避免漏改依赖链上的包</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八版本类型到底怎么选">八、版本类型到底怎么选？<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E5%85%AB%E7%89%88%E6%9C%AC%E7%B1%BB%E5%9E%8B%E5%88%B0%E5%BA%95%E6%80%8E%E4%B9%88%E9%80%89" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>很多人刚用 Changesets 时，最容易纠结的就是：<br>
<!-- -->这次到底该选 <code>patch</code>、<code>minor</code> 还是 <code>major</code>？</p>
<p>你可以先用最简单的方式记：</p>
<ul>
<li><code>patch</code>：修 bug，小修小补，不影响原有用法</li>
<li><code>minor</code>：加新功能，但旧用法还能继续用</li>
<li><code>major</code>：有破坏性变更，旧代码可能会失效</li>
</ul>
<p>也可以直接对应成版本变化：</p>
<ul>
<li><code>patch</code>：<code>1.1.19 -&gt; 1.1.20</code></li>
<li><code>minor</code>：<code>1.1.19 -&gt; 1.2.0</code></li>
<li><code>major</code>：<code>1.1.19 -&gt; 2.0.0</code></li>
</ul>
<p>如果你不确定怎么选，可以问自己一个问题：</p>
<p><strong>“别人升级到我这个版本后，原来的代码会不会出问题？”</strong></p>
<p>如果不会，多半是 <code>patch</code> 或 <code>minor</code>；<br>
<!-- -->如果会，那大概率就是 <code>major</code>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="九changesets-的几个核心好处">九、Changesets 的几个核心好处<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E4%B9%9Dchangesets-%E7%9A%84%E5%87%A0%E4%B8%AA%E6%A0%B8%E5%BF%83%E5%A5%BD%E5%A4%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-版本管理自动化">1. 版本管理自动化<a href="https://blog.kuizuo.me/blog/changesets-introduction#1-%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86%E8%87%AA%E5%8A%A8%E5%8C%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>不用手动计算每个包该升到多少版本，工具会根据 changeset 统一处理。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-changelog-更清晰">2. CHANGELOG 更清晰<a href="https://blog.kuizuo.me/blog/changesets-introduction#2-changelog-%E6%9B%B4%E6%B8%85%E6%99%B0" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>变更记录不是临时回忆出来的，而是每次改动时就顺手记下来的。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-发布时机更灵活">3. 发布时机更灵活<a href="https://blog.kuizuo.me/blog/changesets-introduction#3-%E5%8F%91%E5%B8%83%E6%97%B6%E6%9C%BA%E6%9B%B4%E7%81%B5%E6%B4%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>你可以积累多个 changeset，等到合适的时候一起发版，而不是每改一次就立刻发。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-团队协作更顺">4. 团队协作更顺<a href="https://blog.kuizuo.me/blog/changesets-introduction#4-%E5%9B%A2%E9%98%9F%E5%8D%8F%E4%BD%9C%E6%9B%B4%E9%A1%BA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>每个人都只需要为自己的改动补一张“记录单”，发版时统一汇总，流程更清晰。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-更适合-monorepo">5. 更适合 Monorepo<a href="https://blog.kuizuo.me/blog/changesets-introduction#5-%E6%9B%B4%E9%80%82%E5%90%88-monorepo" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>因为它天然能处理多包版本、内部依赖和 changelog 这些 Monorepo 里的常见问题。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十最后怎么记住-changesets">十、最后怎么记住 Changesets？<a href="https://blog.kuizuo.me/blog/changesets-introduction#%E5%8D%81%E6%9C%80%E5%90%8E%E6%80%8E%E4%B9%88%E8%AE%B0%E4%BD%8F-changesets" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果要把它压缩成一句最容易记住的话，那就是：</p>
<p><strong>Changesets = Monorepo 里的版本管理待办清单。</strong></p>
<p>你可以把整个流程理解成：</p>
<ul>
<li>写代码：完成实际改动</li>
<li>创建 changeset：记录“这次改动将来要怎么发”</li>
<li>执行 <code>version</code>：统一计算版本号并生成 CHANGELOG</li>
<li>执行 <code>publish</code>：真正把包发出去</li>
</ul>
<p>所以它最重要的价值，并不是“帮你多跑了几个命令”，而是让发版这件事从“人肉记忆 + 手工操作”，变成“有记录、有规则、可自动化”的流程。</p>
<p>对于 Monorepo 来说，这一点非常重要。</p>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="Changesets" term="Changesets"/>
        <category label="Monorepo" term="Monorepo"/>
        <category label="pnpm" term="pnpm"/>
        <category label="版本管理" term="版本管理"/>
        <category label="前端工程化" term="前端工程化"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Docker 入门与常用命令：从为什么出现到真正上手]]></title>
        <id>https://blog.kuizuo.me/blog/docker-common-commands</id>
        <link href="https://blog.kuizuo.me/blog/docker-common-commands"/>
        <updated>2026-03-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[参考 Docker 入门思路，系统整理 Docker 为什么会出现、解决了什么问题、核心概念有哪些，以及开发中最常用的 Docker 命令。]]></summary>
        <content type="html"><![CDATA[<p>很多人第一次接触 Docker，都会先记一堆命令：</p>
<ul>
<li><code>docker pull</code></li>
<li><code>docker run</code></li>
<li><code>docker ps</code></li>
<li><code>docker exec</code></li>
</ul>
<p>但如果你不知道 Docker 为什么会出现，这些命令很容易记了又忘。</p>
<p>所以这篇文章不只是整理命令，而是想先把最核心的问题讲清楚：</p>
<ul>
<li>Docker 是为了解决什么问题出现的？</li>
<li>它和虚拟机有什么区别？</li>
<li><code>image</code>、<code>container</code>、<code>Dockerfile</code> 分别是什么？</li>
<li>日常开发里最常用的命令有哪些？</li>
</ul>
<p>如果你把这几个问题理解了，后面的命令就不再是死记硬背。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一docker-为什么会出现">一、Docker 为什么会出现？<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E4%B8%80docker-%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E5%87%BA%E7%8E%B0" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>Docker 出现之前，软件开发里有一个非常经典、也非常头疼的问题：</p>
<p><strong>环境不一致。</strong></p>
<p>你在自己电脑上把项目跑起来了，但换到另一台机器，可能马上就报错。</p>
<p>最常见的场景就是：</p>
<ul>
<li>Node.js 版本不一样</li>
<li>Python 版本不一样</li>
<li>系统库不一样</li>
<li>环境变量不一样</li>
<li>某些依赖在你机器上有，在别人机器上没有</li>
</ul>
<p>于是开发里就常常会出现那句非常著名的话：</p>
<p><strong>“在我机器上是好的。”</strong></p>
<p>这句话翻译一下，其实就是：</p>
<p><strong>项目不是不能跑，而是它依赖的环境太难复制。</strong></p>
<p>所以 Docker 出现的根本背景，不是“大家想学个新工具”，而是：</p>
<p><strong>大家想把应用运行所需的环境，也一起打包带走。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二在-docker-之前为什么虚拟机不够好">二、在 Docker 之前，为什么虚拟机不够好？<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E4%BA%8C%E5%9C%A8-docker-%E4%B9%8B%E5%89%8D%E4%B8%BA%E4%BB%80%E4%B9%88%E8%99%9A%E6%8B%9F%E6%9C%BA%E4%B8%8D%E5%A4%9F%E5%A5%BD" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>为了解决环境问题，最早大家常想到的方案是虚拟机。</p>
<p>虚拟机的思路是：</p>
<p><strong>直接在当前操作系统里，再跑一个完整的操作系统。</strong></p>
<p>比如：</p>
<ul>
<li>在 macOS 里跑 Linux</li>
<li>在 Windows 里跑 Ubuntu</li>
</ul>
<p>这样当然能解决环境问题，因为整个系统都被一起带过去了。</p>
<p>但虚拟机有几个明显缺点：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-太重">1. 太重<a href="https://blog.kuizuo.me/blog/docker-common-commands#1-%E5%A4%AA%E9%87%8D" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>虚拟机里面是完整操作系统，所以会占用比较多的：</p>
<ul>
<li>内存</li>
<li>磁盘</li>
<li>CPU</li>
</ul>
<p>哪怕你只是想跑一个很小的服务，也得先背一整个系统。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-启动慢">2. 启动慢<a href="https://blog.kuizuo.me/blog/docker-common-commands#2-%E5%90%AF%E5%8A%A8%E6%85%A2" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>虚拟机启动，本质上是启动一个完整操作系统，所以速度通常不会太快。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-冗余多">3. 冗余多<a href="https://blog.kuizuo.me/blog/docker-common-commands#3-%E5%86%97%E4%BD%99%E5%A4%9A" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>很多时候你真正想运行的只是一个应用进程，但虚拟机给你的是整套系统级环境。</p>
<p>这就有点像：</p>
<ul>
<li>你只是想煮一碗面</li>
<li>结果别人直接送你一整套厨房</li>
</ul>
<p>所以后来大家就开始追求一种更轻量的方式。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三docker-到底是什么">三、Docker 到底是什么？<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E4%B8%89docker-%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>先说最通俗的理解：</p>
<p><strong>Docker 是一种更轻量的“带环境运行应用”的方式。</strong></p>
<p>它底层基于 Linux 容器技术，但对开发者来说，你可以先把它理解成：</p>
<p><strong>把应用和它依赖的环境一起打包，然后在任何支持 Docker 的机器上用同样的方式运行。</strong></p>
<p>也就是说，Docker 的核心目标是：</p>
<ul>
<li>让环境更一致</li>
<li>让部署更简单</li>
<li>让应用更容易复制、迁移和发布</li>
</ul>
<p>所以很多人会说：</p>
<p><strong>Docker 的价值，不是“让程序能跑”，而是“让程序在哪都能更稳定地跑”。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四docker-解决了什么问题">四、Docker 解决了什么问题？<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%9B%9Bdocker-%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>把它说得再直接一点，Docker 主要解决的是这几类问题。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-环境不一致">1. 环境不一致<a href="https://blog.kuizuo.me/blog/docker-common-commands#1-%E7%8E%AF%E5%A2%83%E4%B8%8D%E4%B8%80%E8%87%B4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这是最核心的问题。</p>
<p>开发、测试、预发、生产，如果环境不一致，就很容易出现：</p>
<ul>
<li>本地可以跑</li>
<li>测试环境不行</li>
<li>线上又出别的问题</li>
</ul>
<p>Docker 通过镜像把运行环境固定下来，让这件事更可控。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-部署麻烦">2. 部署麻烦<a href="https://blog.kuizuo.me/blog/docker-common-commands#2-%E9%83%A8%E7%BD%B2%E9%BA%BB%E7%83%A6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>以前部署一个应用，可能要做很多事：</p>
<ul>
<li>安装运行时</li>
<li>安装依赖</li>
<li>配置环境变量</li>
<li>配置服务</li>
<li>处理版本兼容</li>
</ul>
<p>有了 Docker 后，很多场景可以变成：</p>
<ol>
<li>拉镜像</li>
<li>启动容器</li>
</ol>
<p>部署心智会简单很多。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-迁移成本高">3. 迁移成本高<a href="https://blog.kuizuo.me/blog/docker-common-commands#3-%E8%BF%81%E7%A7%BB%E6%88%90%E6%9C%AC%E9%AB%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>一个项目从开发机迁到测试机、再迁到生产机，过程里最怕的就是“环境差异导致结果不同”。</p>
<p>Docker 把环境和应用一起交付，所以迁移成本更低。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-微服务和隔离运行">4. 微服务和隔离运行<a href="https://blog.kuizuo.me/blog/docker-common-commands#4-%E5%BE%AE%E6%9C%8D%E5%8A%A1%E5%92%8C%E9%9A%94%E7%A6%BB%E8%BF%90%E8%A1%8C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>如果一台机器上要跑多个服务，Docker 也很有优势：</p>
<ul>
<li>每个服务一个容器</li>
<li>互相隔离</li>
<li>启停方便</li>
<li>适合扩容缩容</li>
</ul>
<p>这也是它为什么在云原生和微服务里这么常见。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五docker-和虚拟机到底有什么区别">五、Docker 和虚拟机到底有什么区别？<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E4%BA%94docker-%E5%92%8C%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%88%B0%E5%BA%95%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这是新手最常问的问题之一。</p>
<p>你可以先记这个版本：</p>
<ul>
<li><strong>虚拟机</strong>：模拟一整台机器，里面跑完整操作系统</li>
<li><strong>Docker 容器</strong>：不是模拟整台机器，而是隔离应用进程</li>
</ul>
<p>所以两者的区别通常体现在：</p>
<table><thead><tr><th>维度</th><th>虚拟机</th><th>Docker</th></tr></thead><tbody><tr><td>隔离对象</td><td>完整操作系统</td><td>应用进程</td></tr><tr><td>启动速度</td><td>较慢</td><td>很快</td></tr><tr><td>资源占用</td><td>较高</td><td>较低</td></tr><tr><td>体积</td><td>较大</td><td>较小</td></tr><tr><td>适合场景</td><td>需要完整 OS 隔离</td><td>应用打包、部署、微服务</td></tr></tbody></table>
<p>你可以把它理解成：</p>
<ul>
<li>虚拟机更像“再搬来一整套房子”</li>
<li>Docker 更像“在同一栋楼里隔出一个独立房间”</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六docker-里最核心的几个概念">六、Docker 里最核心的几个概念<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%85%ADdocker-%E9%87%8C%E6%9C%80%E6%A0%B8%E5%BF%83%E7%9A%84%E5%87%A0%E4%B8%AA%E6%A6%82%E5%BF%B5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>学 Docker，最重要的不是先记命令，而是先把下面几个概念分清楚。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-image镜像文件">1. Image：镜像文件<a href="https://blog.kuizuo.me/blog/docker-common-commands#1-image%E9%95%9C%E5%83%8F%E6%96%87%E4%BB%B6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p><code>image</code> 可以理解成：</p>
<p><strong>一个用来创建容器的模板。</strong></p>
<p>它里面通常包含：</p>
<ul>
<li>应用代码</li>
<li>运行时环境</li>
<li>系统依赖</li>
<li>启动配置</li>
</ul>
<p>比如一个 Node.js 项目的镜像，里面可能已经包含：</p>
<ul>
<li>Linux 基础环境</li>
<li>Node.js</li>
<li>项目源码</li>
<li>npm 依赖</li>
</ul>
<p>所以镜像不是“正在运行的程序”，而更像是：</p>
<p><strong>一个可复制、可分发的运行快照。</strong></p>
<p>同一个镜像，可以启动多个容器。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-container容器">2. Container：容器<a href="https://blog.kuizuo.me/blog/docker-common-commands#2-container%E5%AE%B9%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p><code>container</code> 可以理解成：</p>
<p><strong>镜像运行之后的实例。</strong></p>
<p>也就是说：</p>
<ul>
<li><code>image</code> 是模板</li>
<li><code>container</code> 是模板跑起来后的实际对象</li>
</ul>
<p>类比一下更好理解：</p>
<ul>
<li>镜像像“类”</li>
<li>容器像“对象实例”</li>
</ul>
<p>同一个镜像可以创建多个容器，它们彼此独立运行。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-dockerfile镜像说明书">3. Dockerfile：镜像说明书<a href="https://blog.kuizuo.me/blog/docker-common-commands#3-dockerfile%E9%95%9C%E5%83%8F%E8%AF%B4%E6%98%8E%E4%B9%A6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>如果你想自己制作镜像，就需要 <code>Dockerfile</code>。</p>
<p>它本质上是一个文本文件，用来描述：</p>
<ul>
<li>这个镜像基于什么基础镜像</li>
<li>要拷贝哪些文件</li>
<li>要执行哪些安装命令</li>
<li>容器启动后默认运行什么命令</li>
</ul>
<p>比如一个很常见的 <code>Dockerfile</code>：</p>
<div class="language-dockerfile codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-dockerfile codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">FROM node:20</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">WORKDIR /app</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">COPY . .</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">RUN npm install</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">EXPOSE 3000</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">CMD ["npm", "start"]</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这段配置的意思就是：</p>
<ul>
<li>基于 <code>node:20</code> 这个镜像</li>
<li>工作目录设为 <code>/app</code></li>
<li>把当前目录文件复制进去</li>
<li>安装依赖</li>
<li>暴露 3000 端口</li>
<li>容器启动后执行 <code>npm start</code></li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-docker-hub镜像仓库">4. Docker Hub：镜像仓库<a href="https://blog.kuizuo.me/blog/docker-common-commands#4-docker-hub%E9%95%9C%E5%83%8F%E4%BB%93%E5%BA%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>镜像做好以后，可以上传到仓库。</p>
<p>最常见的是 <a href="https://hub.docker.com/" target="_blank" rel="noopener noreferrer">Docker Hub</a>。</p>
<p>你可以把它理解成 Docker 世界里的“npm / GitHub Releases”。</p>
<p>常见操作就是：</p>
<ul>
<li>从仓库拉镜像</li>
<li>把自己的镜像推上去</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七docker-的基本工作流程">七、Docker 的基本工作流程<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E4%B8%83docker-%E7%9A%84%E5%9F%BA%E6%9C%AC%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果用最简单的流程来理解 Docker，大概就是：</p>
<ol>
<li>写好 <code>Dockerfile</code></li>
<li>构建出 <code>image</code></li>
<li>用 <code>image</code> 运行 <code>container</code></li>
<li>容器里启动应用</li>
</ol>
<p>所以链路其实很清楚：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">Dockerfile -&gt; Image -&gt; Container</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这也是为什么很多命令都围绕这三件事展开：</p>
<ul>
<li>管理镜像</li>
<li>管理容器</li>
<li>构建镜像</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八先安装-docker">八、先安装 Docker<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%85%AB%E5%85%88%E5%AE%89%E8%A3%85-docker" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果你是 macOS 用户，最简单的方式通常是安装 Docker Desktop。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-下载-docker-desktop">1. 下载 Docker Desktop<a href="https://blog.kuizuo.me/blog/docker-common-commands#1-%E4%B8%8B%E8%BD%BD-docker-desktop" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>打开 <a href="https://www.docker.com/products/docker-desktop" target="_blank" rel="noopener noreferrer">Docker 官网</a></li>
<li>下载适用于 macOS 的 Docker Desktop</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-安装">2. 安装<a href="https://blog.kuizuo.me/blog/docker-common-commands#2-%E5%AE%89%E8%A3%85" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>安装方式和普通 macOS 应用差不多：</p>
<ul>
<li>打开下载的 <code>.dmg</code></li>
<li>将 Docker 拖到 <code>Applications</code></li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-启动-docker">3. 启动 Docker<a href="https://blog.kuizuo.me/blog/docker-common-commands#3-%E5%90%AF%E5%8A%A8-docker" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>可以直接从应用列表打开，也可以用命令：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">open</span><span class="token plain"> /Applications/Docker.app</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-验证安装">4. 验证安装<a href="https://blog.kuizuo.me/blog/docker-common-commands#4-%E9%AA%8C%E8%AF%81%E5%AE%89%E8%A3%85" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> version</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> info</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>如果能正常输出版本和系统信息，说明 Docker 基本已经装好了。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="九新手最该掌握的-docker-核心命令">九、新手最该掌握的 Docker 核心命令<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E4%B9%9D%E6%96%B0%E6%89%8B%E6%9C%80%E8%AF%A5%E6%8E%8C%E6%8F%A1%E7%9A%84-docker-%E6%A0%B8%E5%BF%83%E5%91%BD%E4%BB%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>下面这部分，我按“日常使用频率”来整理。</p>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-查看帮助和环境信息">1. 查看帮助和环境信息<a href="https://blog.kuizuo.me/blog/docker-common-commands#1-%E6%9F%A5%E7%9C%8B%E5%B8%AE%E5%8A%A9%E5%92%8C%E7%8E%AF%E5%A2%83%E4%BF%A1%E6%81%AF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> version</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> info</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--help</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">&lt;</span><span class="token plain">命令</span><span class="token operator" style="color:hsl(221, 87%, 60%)">&gt;</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--help</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这几个命令适合在你不确定当前 Docker 状态时先看一眼。</p>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-镜像相关命令">2. 镜像相关命令<a href="https://blog.kuizuo.me/blog/docker-common-commands#2-%E9%95%9C%E5%83%8F%E7%9B%B8%E5%85%B3%E5%91%BD%E4%BB%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="查看本地镜像">查看本地镜像<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E6%9F%A5%E7%9C%8B%E6%9C%AC%E5%9C%B0%E9%95%9C%E5%83%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image </span><span class="token function" style="color:hsl(221, 87%, 60%)">ls</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>常见简写也可以用：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> images</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>常用参数：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image </span><span class="token function" style="color:hsl(221, 87%, 60%)">ls</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-a</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image </span><span class="token function" style="color:hsl(221, 87%, 60%)">ls</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-q</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="搜索镜像">搜索镜像<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E6%90%9C%E7%B4%A2%E9%95%9C%E5%83%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> search nginx</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> search mysql </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--filter</span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain">STARS</span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token number" style="color:hsl(35, 99%, 36%)">3000</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="拉取镜像">拉取镜像<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E6%8B%89%E5%8F%96%E9%95%9C%E5%83%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image pull nginx</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image pull mysql:8.0</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>很多时候也会直接简写成：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> pull nginx</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="删除镜像">删除镜像<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%88%A0%E9%99%A4%E9%95%9C%E5%83%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image </span><span class="token function" style="color:hsl(221, 87%, 60%)">rm</span><span class="token plain"> 镜像ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image </span><span class="token function" style="color:hsl(221, 87%, 60%)">rm</span><span class="token plain"> nginx:latest</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image </span><span class="token function" style="color:hsl(221, 87%, 60%)">rm</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-f</span><span class="token plain"> 镜像ID</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-容器相关命令">3. 容器相关命令<a href="https://blog.kuizuo.me/blog/docker-common-commands#3-%E5%AE%B9%E5%99%A8%E7%9B%B8%E5%85%B3%E5%91%BD%E4%BB%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="运行容器">运行容器<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E8%BF%90%E8%A1%8C%E5%AE%B9%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这是最重要的命令之一：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> container run 镜像名</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>常见写法：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run hello-world</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-it</span><span class="token plain"> ubuntu </span><span class="token function" style="color:hsl(221, 87%, 60%)">bash</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-d</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-p</span><span class="token plain"> </span><span class="token number" style="color:hsl(35, 99%, 36%)">80</span><span class="token plain">:80 </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--name</span><span class="token plain"> my-nginx nginx</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--rm</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-it</span><span class="token plain"> ubuntu:22.04 </span><span class="token function" style="color:hsl(221, 87%, 60%)">bash</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>常用参数一定要记住：</p>
<table><thead><tr><th>参数</th><th>作用</th><th>常见场景</th></tr></thead><tbody><tr><td><code>-it</code></td><td>交互式进入容器</td><td>调试、进入 shell</td></tr><tr><td><code>-d</code></td><td>后台运行</td><td>跑服务</td></tr><tr><td><code>--name</code></td><td>指定容器名</td><td>方便管理</td></tr><tr><td><code>-p 主机端口:容器端口</code></td><td>端口映射</td><td>对外暴露服务</td></tr><tr><td><code>-v</code></td><td>挂载数据卷</td><td>持久化数据</td></tr><tr><td><code>--rm</code></td><td>容器退出后自动删除</td><td>临时调试</td></tr></tbody></table>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="查看容器">查看容器<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E6%9F%A5%E7%9C%8B%E5%AE%B9%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> container </span><span class="token function" style="color:hsl(221, 87%, 60%)">ls</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> container </span><span class="token function" style="color:hsl(221, 87%, 60%)">ls</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--all</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>常见简写：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">ps</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">ps</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-a</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">ps</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-q</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">ps</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-n</span><span class="token plain"> </span><span class="token number" style="color:hsl(35, 99%, 36%)">2</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="启动停止重启容器">启动、停止、重启容器<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%90%AF%E5%8A%A8%E5%81%9C%E6%AD%A2%E9%87%8D%E5%90%AF%E5%AE%B9%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> container start 容器ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> container stop 容器ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> container restart 容器ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> container </span><span class="token function" style="color:hsl(221, 87%, 60%)">kill</span><span class="token plain"> 容器ID</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>简单理解：</p>
<ul>
<li><code>stop</code>：尽量优雅停止</li>
<li><code>kill</code>：强制立即停止</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="删除容器">删除容器<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%88%A0%E9%99%A4%E5%AE%B9%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> container </span><span class="token function" style="color:hsl(221, 87%, 60%)">rm</span><span class="token plain"> 容器ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> container </span><span class="token function" style="color:hsl(221, 87%, 60%)">rm</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-f</span><span class="token plain"> 容器ID</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>批量删除：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">rm</span><span class="token plain"> </span><span class="token variable" style="color:hsl(221, 87%, 60%)">$(</span><span class="token variable function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token variable" style="color:hsl(221, 87%, 60%)"> </span><span class="token variable function" style="color:hsl(221, 87%, 60%)">ps</span><span class="token variable" style="color:hsl(221, 87%, 60%)"> </span><span class="token variable parameter variable" style="color:hsl(221, 87%, 60%)">-aq</span><span class="token variable" style="color:hsl(221, 87%, 60%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">rm</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-f</span><span class="token plain"> </span><span class="token variable" style="color:hsl(221, 87%, 60%)">$(</span><span class="token variable function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token variable" style="color:hsl(221, 87%, 60%)"> </span><span class="token variable function" style="color:hsl(221, 87%, 60%)">ps</span><span class="token variable" style="color:hsl(221, 87%, 60%)"> </span><span class="token variable parameter variable" style="color:hsl(221, 87%, 60%)">-aq</span><span class="token variable" style="color:hsl(221, 87%, 60%)">)</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-查看日志和容器信息">4. 查看日志和容器信息<a href="https://blog.kuizuo.me/blog/docker-common-commands#4-%E6%9F%A5%E7%9C%8B%E6%97%A5%E5%BF%97%E5%92%8C%E5%AE%B9%E5%99%A8%E4%BF%A1%E6%81%AF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="查看日志">查看日志<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E6%9F%A5%E7%9C%8B%E6%97%A5%E5%BF%97" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> logs 容器ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> logs </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-f</span><span class="token plain"> 容器ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> logs </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--tail</span><span class="token plain"> </span><span class="token number" style="color:hsl(35, 99%, 36%)">50</span><span class="token plain"> 容器ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> logs </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-t</span><span class="token plain"> 容器ID</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="查看详细信息">查看详细信息<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E6%9F%A5%E7%9C%8B%E8%AF%A6%E7%BB%86%E4%BF%A1%E6%81%AF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">top</span><span class="token plain"> 容器ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> inspect 容器ID</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> stats 容器ID</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这些命令很适合排查：</p>
<ul>
<li>容器到底有没有启动</li>
<li>容器里跑了哪些进程</li>
<li>资源占用高不高</li>
<li>配置是否符合预期</li>
</ul>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-进入正在运行的容器">5. 进入正在运行的容器<a href="https://blog.kuizuo.me/blog/docker-common-commands#5-%E8%BF%9B%E5%85%A5%E6%AD%A3%E5%9C%A8%E8%BF%90%E8%A1%8C%E7%9A%84%E5%AE%B9%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>最推荐的方式是：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token builtin class-name" style="color:hsl(35, 99%, 36%)">exec</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-it</span><span class="token plain"> 容器ID /bin/bash</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>如果容器里没有 bash，也可以试：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token builtin class-name" style="color:hsl(35, 99%, 36%)">exec</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-it</span><span class="token plain"> 容器ID /bin/sh</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>另一个命令是：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> attach 容器ID</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>两者区别可以简单记成：</p>
<ul>
<li><code>docker exec</code>：开启一个新的终端，更常用</li>
<li><code>docker attach</code>：连接到当前主进程所在终端，容易互相影响</li>
</ul>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="6-文件复制">6. 文件复制<a href="https://blog.kuizuo.me/blog/docker-common-commands#6-%E6%96%87%E4%BB%B6%E5%A4%8D%E5%88%B6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="从容器复制到主机">从容器复制到主机<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E4%BB%8E%E5%AE%B9%E5%99%A8%E5%A4%8D%E5%88%B6%E5%88%B0%E4%B8%BB%E6%9C%BA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">cp</span><span class="token plain"> 容器ID:/path/to/file </span><span class="token builtin class-name" style="color:hsl(35, 99%, 36%)">.</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="从主机复制到容器">从主机复制到容器<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E4%BB%8E%E4%B8%BB%E6%9C%BA%E5%A4%8D%E5%88%B6%E5%88%B0%E5%AE%B9%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">cp</span><span class="token plain"> ./local.txt 容器ID:/app/local.txt</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这个命令在临时排查问题时非常好用。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十先用一个最简单的例子hello-world">十、先用一个最简单的例子：hello-world<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%8D%81%E5%85%88%E7%94%A8%E4%B8%80%E4%B8%AA%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84%E4%BE%8B%E5%AD%90hello-world" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>第一次接触 Docker，最适合先跑这个：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run hello-world</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这个命令会做几件事：</p>
<ol>
<li>如果本地没有 <code>hello-world</code> 镜像，就先自动拉取</li>
<li>基于这个镜像创建容器</li>
<li>运行容器</li>
<li>输出一段欢迎信息</li>
<li>容器结束退出</li>
</ol>
<p>这个例子最适合用来理解：</p>
<p><strong>镜像不是程序本身，镜像运行后才会变成容器。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十一学-docker-一定要会-dockerfile">十一、学 Docker 一定要会 Dockerfile<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%8D%81%E4%B8%80%E5%AD%A6-docker-%E4%B8%80%E5%AE%9A%E8%A6%81%E4%BC%9A-dockerfile" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果只会 <code>docker run nginx</code>，那你只是会“用别人的镜像”。</p>
<p>真正进入日常开发，最重要的是：</p>
<p><strong>把你自己的项目做成镜像。</strong></p>
<p>这就离不开 <code>Dockerfile</code>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一个最常见的-nodejs-示例">一个最常见的 Node.js 示例<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E4%B8%80%E4%B8%AA%E6%9C%80%E5%B8%B8%E8%A7%81%E7%9A%84-nodejs-%E7%A4%BA%E4%BE%8B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-dockerfile codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-dockerfile codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">FROM node:20</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">WORKDIR /app</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">COPY . .</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">RUN npm install</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">EXPOSE 3000</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">CMD ["npm", "start"]</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="对应的构建命令">对应的构建命令<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%AF%B9%E5%BA%94%E7%9A%84%E6%9E%84%E5%BB%BA%E5%91%BD%E4%BB%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image build </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-t</span><span class="token plain"> my-app </span><span class="token builtin class-name" style="color:hsl(35, 99%, 36%)">.</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>也可以带版本号：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image build </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-t</span><span class="token plain"> my-app:1.0.0 </span><span class="token builtin class-name" style="color:hsl(35, 99%, 36%)">.</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="运行这个镜像">运行这个镜像<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E8%BF%90%E8%A1%8C%E8%BF%99%E4%B8%AA%E9%95%9C%E5%83%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-p</span><span class="token plain"> </span><span class="token number" style="color:hsl(35, 99%, 36%)">3000</span><span class="token plain">:3000 </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--name</span><span class="token plain"> my-app-container my-app</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>所以最常见的一套动作就是：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> build </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-t</span><span class="token plain"> my-app </span><span class="token builtin class-name" style="color:hsl(35, 99%, 36%)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-p</span><span class="token plain"> </span><span class="token number" style="color:hsl(35, 99%, 36%)">3000</span><span class="token plain">:3000 my-app</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十二run-和-cmd-有什么区别">十二、<code>RUN</code> 和 <code>CMD</code> 有什么区别？<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%8D%81%E4%BA%8Crun-%E5%92%8C-cmd-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这是 Dockerfile 里最容易混淆的一组概念。</p>
<p>你可以这样记：</p>
<ul>
<li><code>RUN</code>：<strong>构建镜像时执行</strong></li>
<li><code>CMD</code>：<strong>启动容器时执行</strong></li>
</ul>
<p>例如：</p>
<div class="language-dockerfile codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-dockerfile codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">RUN npm install</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">CMD ["npm", "start"]</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>含义就是：</p>
<ul>
<li>构建镜像时先安装依赖</li>
<li>容器启动后再运行应用</li>
</ul>
<p>所以：</p>
<ul>
<li><code>RUN</code> 更像“制作镜像过程中的步骤”</li>
<li><code>CMD</code> 更像“容器启动后的默认命令”</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十三一些非常实用的组合命令">十三、一些非常实用的组合命令<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%8D%81%E4%B8%89%E4%B8%80%E4%BA%9B%E9%9D%9E%E5%B8%B8%E5%AE%9E%E7%94%A8%E7%9A%84%E7%BB%84%E5%90%88%E5%91%BD%E4%BB%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>下面这些在日常清理和排查里非常常见。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="停止并删除所有容器">停止并删除所有容器<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%81%9C%E6%AD%A2%E5%B9%B6%E5%88%A0%E9%99%A4%E6%89%80%E6%9C%89%E5%AE%B9%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> stop </span><span class="token variable" style="color:hsl(221, 87%, 60%)">$(</span><span class="token variable function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token variable" style="color:hsl(221, 87%, 60%)"> </span><span class="token variable function" style="color:hsl(221, 87%, 60%)">ps</span><span class="token variable" style="color:hsl(221, 87%, 60%)"> </span><span class="token variable parameter variable" style="color:hsl(221, 87%, 60%)">-aq</span><span class="token variable" style="color:hsl(221, 87%, 60%)">)</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">rm</span><span class="token plain"> </span><span class="token variable" style="color:hsl(221, 87%, 60%)">$(</span><span class="token variable function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token variable" style="color:hsl(221, 87%, 60%)"> </span><span class="token variable function" style="color:hsl(221, 87%, 60%)">ps</span><span class="token variable" style="color:hsl(221, 87%, 60%)"> </span><span class="token variable parameter variable" style="color:hsl(221, 87%, 60%)">-aq</span><span class="token variable" style="color:hsl(221, 87%, 60%)">)</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="清理未使用镜像">清理未使用镜像<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E6%B8%85%E7%90%86%E6%9C%AA%E4%BD%BF%E7%94%A8%E9%95%9C%E5%83%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> image prune </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-a</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="清理系统垃圾">清理系统垃圾<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E6%B8%85%E7%90%86%E7%B3%BB%E7%BB%9F%E5%9E%83%E5%9C%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> system prune </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-a</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="限制容器资源">限制容器资源<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E9%99%90%E5%88%B6%E5%AE%B9%E5%99%A8%E8%B5%84%E6%BA%90" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-d</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--memory</span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token string" style="color:hsl(119, 34%, 47%)">"500m"</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--cpus</span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token string" style="color:hsl(119, 34%, 47%)">"1.0"</span><span class="token plain"> nginx</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-d</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">--memory</span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token string" style="color:hsl(119, 34%, 47%)">"500m"</span><span class="token plain"> --memory-swap</span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token string" style="color:hsl(119, 34%, 47%)">"1g"</span><span class="token plain"> nginx</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十四新手最容易踩的几个坑">十四、新手最容易踩的几个坑<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%8D%81%E5%9B%9B%E6%96%B0%E6%89%8B%E6%9C%80%E5%AE%B9%E6%98%93%E8%B8%A9%E7%9A%84%E5%87%A0%E4%B8%AA%E5%9D%91" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-容器为什么一启动就退出">1. 容器为什么一启动就退出？<a href="https://blog.kuizuo.me/blog/docker-common-commands#1-%E5%AE%B9%E5%99%A8%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%80%E5%90%AF%E5%8A%A8%E5%B0%B1%E9%80%80%E5%87%BA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>因为 Docker 容器必须有前台进程在运行。</p>
<p>如果主进程结束，容器就结束。</p>
<p>比如这条命令：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-d</span><span class="token plain"> centos</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>很多情况下容器会立刻退出，因为它没有持续运行的前台任务。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-为什么访问不到服务">2. 为什么访问不到服务？<a href="https://blog.kuizuo.me/blog/docker-common-commands#2-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AE%BF%E9%97%AE%E4%B8%8D%E5%88%B0%E6%9C%8D%E5%8A%A1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>通常先检查这几件事：</p>
<ul>
<li>容器内服务是否真的启动了</li>
<li>是否做了端口映射</li>
<li>映射端口是否写反了</li>
<li>服务是否监听在正确地址</li>
</ul>
<p>最常见的端口映射写法是：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:hsl(221, 87%, 60%)">-p</span><span class="token plain"> </span><span class="token number" style="color:hsl(35, 99%, 36%)">8080</span><span class="token plain">:80 nginx</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>意思是：</p>
<ul>
<li>主机访问 <code>8080</code></li>
<li>转发到容器里的 <code>80</code></li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-为什么改了代码容器里没变化">3. 为什么改了代码，容器里没变化？<a href="https://blog.kuizuo.me/blog/docker-common-commands#3-%E4%B8%BA%E4%BB%80%E4%B9%88%E6%94%B9%E4%BA%86%E4%BB%A3%E7%A0%81%E5%AE%B9%E5%99%A8%E9%87%8C%E6%B2%A1%E5%8F%98%E5%8C%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>因为镜像不是自动更新的。</p>
<p>如果你改了源码，通常需要：</p>
<ol>
<li>重新构建镜像</li>
<li>重新运行容器</li>
</ol>
<p>或者直接通过挂载卷的方式在开发时同步代码。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十五如果你只想先记住-docker记这几句话就够了">十五、如果你只想先记住 Docker，记这几句话就够了<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E5%8D%81%E4%BA%94%E5%A6%82%E6%9E%9C%E4%BD%A0%E5%8F%AA%E6%83%B3%E5%85%88%E8%AE%B0%E4%BD%8F-docker%E8%AE%B0%E8%BF%99%E5%87%A0%E5%8F%A5%E8%AF%9D%E5%B0%B1%E5%A4%9F%E4%BA%86" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>可以把 Docker 先理解成这样：</p>
<ul>
<li><strong>Docker 解决的是环境一致性问题</strong></li>
<li><strong>Image 是模板，Container 是运行中的实例</strong></li>
<li><strong>Dockerfile 是制作镜像的说明书</strong></li>
<li><strong>最常用的命令就是 <code>pull</code>、<code>build</code>、<code>run</code>、<code>ps</code>、<code>logs</code>、<code>exec</code></strong></li>
</ul>
<p>如果再压缩成一条主线：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">写 Dockerfile -&gt; build 成 image -&gt; run 成 container</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>你只要把这条线记住，Docker 的学习就不会乱。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="总结">总结<a href="https://blog.kuizuo.me/blog/docker-common-commands#%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>Docker 真正重要的地方，不是让你多学了一堆命令，而是它改变了软件交付方式：</p>
<ul>
<li>从“把代码给别人”变成“把运行环境和代码一起给别人”</li>
<li>从“这台机器能跑”变成“换台机器也尽量能跑”</li>
<li>从“手工部署一堆依赖”变成“通过镜像统一交付”</li>
</ul>
<p>所以如果你是前端、后端、测试、运维，只要开始接触本地环境、部署、微服务或 CI/CD，Docker 基本都会变成绕不过去的工具。</p>
<p>如果后续你还想继续深入，一个很自然的学习顺序是：</p>
<ol>
<li>先熟悉本文这些核心命令</li>
<li>再学 <code>Dockerfile</code></li>
<li>再学 <code>Docker Compose</code></li>
<li>最后再看容器编排和云原生相关内容</li>
</ol>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="Docker" term="Docker"/>
        <category label="容器" term="容器"/>
        <category label="运维" term="运维"/>
        <category label="开发工具" term="开发工具"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[npm、Yarn、pnpm 到底有什么区别？一篇讲明白]]></title>
        <id>https://blog.kuizuo.me/blog/npm-yarn-pnpm</id>
        <link href="https://blog.kuizuo.me/blog/npm-yarn-pnpm"/>
        <updated>2026-03-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[用通俗的方式讲清 npm、Yarn、pnpm 的区别、演进原因、优缺点，以及新项目到底该怎么选。]]></summary>
        <content type="html"><![CDATA[<p>如果你刚接触前端工程化，第一次看到 <code>npm install</code>、<code>yarn install</code>、<code>pnpm install</code>，大概率会有一个疑问：</p>
<p><strong>它们不都是“装包”的吗？为什么会有三个？</strong></p>
<p>答案是：<strong>它们做的是同一件事，但解决问题的方式不一样。</strong></p>
<ul>
<li><code>npm</code> 是 Node.js 官方自带的包管理器，历史最久，生态最稳。</li>
<li><code>Yarn</code> 是在 npm 早期体验不够好的时候出现的，重点解决“慢”和“不稳定”。</li>
<li><code>pnpm</code> 是在 Yarn 和 npm 继续发展之后，进一步解决“磁盘浪费”和“依赖不严谨”的问题。</li>
</ul>
<p>这篇文章不讲太多绕人的术语，只用一条主线讲清楚：</p>
<p><strong>为什么会从 npm 发展到 Yarn，再发展到 pnpm？</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一先说结论新手该怎么理解这三个工具">一、先说结论：新手该怎么理解这三个工具？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E4%B8%80%E5%85%88%E8%AF%B4%E7%BB%93%E8%AE%BA%E6%96%B0%E6%89%8B%E8%AF%A5%E6%80%8E%E4%B9%88%E7%90%86%E8%A7%A3%E8%BF%99%E4%B8%89%E4%B8%AA%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>你可以先把它们理解成三个版本的“快递仓库管理系统”：</p>
<ul>
<li><code>npm</code>：最早上线，先把“包能装下来”这件事解决了。</li>
<li><code>Yarn</code>：发现 npm 早期装得慢、装得不稳，于是把流程优化了。</li>
<li><code>pnpm</code>：继续往前走，开始追求“更省空间、更快、更严格”。</li>
</ul>
<p>所以它们并不是谁“取代”谁，而是<strong>在不同阶段针对不同痛点做优化</strong>。</p>
<p>如果只想先记一句话：</p>
<ul>
<li>小项目、通用项目：<code>npm</code> 完全够用。</li>
<li>注重安装速度、磁盘空间、Monorepo：更推荐 <code>pnpm</code>。</li>
<li><code>Yarn</code> 依然有人用，但新项目里相较前两者已经没那么常见。</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二它们到底是干什么的">二、它们到底是干什么的？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E4%BA%8C%E5%AE%83%E4%BB%AC%E5%88%B0%E5%BA%95%E6%98%AF%E5%B9%B2%E4%BB%80%E4%B9%88%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>三者本质上都是 <strong>JavaScript / Node.js 的包管理器</strong>，主要做这几件事：</p>
<ul>
<li>安装依赖</li>
<li>删除依赖</li>
<li>更新依赖</li>
<li>锁定依赖版本</li>
<li>管理 <code>node_modules</code></li>
</ul>
<p>比如你写 React 项目时要安装 <code>react</code>、<code>vite</code>、<code>axios</code>，这些工具都会帮你从包仓库下载代码，并放到项目里可用的位置。</p>
<p>最常见的命令长这样：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">npm</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">install</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">yarn</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">install</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">install</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>表面看它们差不多，但<strong>内部怎么存、怎么装、怎么复用依赖</strong>，差别很大。这也是为什么它们的速度、磁盘占用、依赖管理体验会不一样。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三为什么会先有-npm再有-yarn-和-pnpm">三、为什么会先有 npm，再有 Yarn 和 pnpm？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E4%B8%89%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E5%85%88%E6%9C%89-npm%E5%86%8D%E6%9C%89-yarn-%E5%92%8C-pnpm" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>理解这件事最好的方法，不是背定义，而是看它们分别解决了什么问题。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-npm-先解决从无到有">1. npm 先解决“从无到有”<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#1-npm-%E5%85%88%E8%A7%A3%E5%86%B3%E4%BB%8E%E6%97%A0%E5%88%B0%E6%9C%89" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>2010 年，<code>npm</code> 随 Node.js 一起出现。那个阶段最重要的不是“快不快”，而是：</p>
<p><strong>JavaScript 终于有了一个像样的包管理系统。</strong></p>
<p>开发者可以很方便地发布包、安装包、复用社区代码。这个意义非常大，Node.js 生态能爆发，npm 功不可没。</p>
<p>但随着项目越来越大，问题也开始暴露。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-yarn-解决太慢太不稳">2. Yarn 解决“太慢、太不稳”<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#2-yarn-%E8%A7%A3%E5%86%B3%E5%A4%AA%E6%85%A2%E5%A4%AA%E4%B8%8D%E7%A8%B3" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>后来大家发现，早期 npm 在大型项目里有几个明显问题：</p>
<ul>
<li>安装速度慢</li>
<li>依赖容易重复安装</li>
<li>不同机器安装结果不一定完全一致</li>
</ul>
<p>于是 2016 年，Facebook 推出了 <code>Yarn</code>。它的重点很明确：</p>
<p><strong>先把安装体验做得更快、更稳。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-pnpm-解决太占空间依赖不够严格">3. pnpm 解决“太占空间、依赖不够严格”<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#3-pnpm-%E8%A7%A3%E5%86%B3%E5%A4%AA%E5%8D%A0%E7%A9%BA%E9%97%B4%E4%BE%9D%E8%B5%96%E4%B8%8D%E5%A4%9F%E4%B8%A5%E6%A0%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>再往后，大家又发现一个问题：</p>
<p><strong>你电脑里多个项目，往往装了大量重复依赖，但这些文件其实一模一样。</strong></p>
<p>而且 npm / Yarn 早期常见的扁平化依赖结构，还会带来“幽灵依赖”问题。</p>
<p>于是 2017 年，<code>pnpm</code> 出现了。它的核心思路是：</p>
<p><strong>同样的包，全局只存一份；项目里通过链接去复用。</strong></p>
<p>这让它在磁盘占用、安装速度、Monorepo 场景上都非常亮眼。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四npm-做了什么它为什么会遇到问题">四、npm 做了什么？它为什么会遇到问题？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E5%9B%9Bnpm-%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88%E5%AE%83%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E9%81%87%E5%88%B0%E9%97%AE%E9%A2%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-npm-最早的价值把包管理标准化">1. npm 最早的价值：把包管理标准化<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#1-npm-%E6%9C%80%E6%97%A9%E7%9A%84%E4%BB%B7%E5%80%BC%E6%8A%8A%E5%8C%85%E7%AE%A1%E7%90%86%E6%A0%87%E5%87%86%E5%8C%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>在 npm 出现之前，前端和 Node.js 世界的依赖管理非常零散。npm 出现后，开发者终于可以：</p>
<ul>
<li>用统一命令安装依赖</li>
<li>通过 <code>package.json</code> 管理项目依赖</li>
<li>从公共仓库下载别人发布的包</li>
</ul>
<p>这个阶段的 npm，最大的贡献是：</p>
<p><strong>让整个 Node.js 生态跑起来。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-早期-npm-的问题依赖越多项目越重">2. 早期 npm 的问题：依赖越多，项目越重<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#2-%E6%97%A9%E6%9C%9F-npm-%E7%9A%84%E9%97%AE%E9%A2%98%E4%BE%9D%E8%B5%96%E8%B6%8A%E5%A4%9A%E9%A1%B9%E7%9B%AE%E8%B6%8A%E9%87%8D" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>早期 npm 会采用更偏“嵌套”的依赖结构。简单理解就是：</p>
<p><strong>每个包都可能在自己的目录里再放一份依赖。</strong></p>
<p>结果就是，同一个依赖如果有多个版本，甚至相同版本，也可能在不同位置被重复安装。</p>
<p>像这样：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">node_modules/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">├── package-a/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">│   └── node_modules/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">│       └── package-b@1.0.0</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── package-d/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    └── node_modules/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">        └── package-b@2.0.0</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这会带来几个问题：</p>
<ul>
<li><code>node_modules</code> 很大</li>
<li>安装时间越来越长</li>
<li>依赖关系越来越复杂</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-npm-后来做了哪些改进">3. npm 后来做了哪些改进？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#3-npm-%E5%90%8E%E6%9D%A5%E5%81%9A%E4%BA%86%E5%93%AA%E4%BA%9B%E6%94%B9%E8%BF%9B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>后来 npm 也持续升级，比如：</p>
<ul>
<li>使用更扁平的依赖结构，减少重复安装</li>
<li>引入 <code>package-lock.json</code>，让依赖版本更可控</li>
<li>持续优化安装性能</li>
<li>支持 <code>workspaces</code></li>
</ul>
<p>所以今天的 npm，已经不是“很早以前那个慢吞吞的 npm”了。</p>
<p>它现在依然是<strong>最通用、最稳妥、门槛最低</strong>的选择。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五yarn-为什么会火它到底改进了什么">五、Yarn 为什么会火？它到底改进了什么？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E4%BA%94yarn-%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E7%81%AB%E5%AE%83%E5%88%B0%E5%BA%95%E6%94%B9%E8%BF%9B%E4%BA%86%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-yarn-出现的背景很现实">1. Yarn 出现的背景很现实<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#1-yarn-%E5%87%BA%E7%8E%B0%E7%9A%84%E8%83%8C%E6%99%AF%E5%BE%88%E7%8E%B0%E5%AE%9E" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>当项目依赖变多之后，团队最怕的不是“不会装”，而是：</p>
<ul>
<li>装得太慢</li>
<li>同事之间装出来结果不一样</li>
<li>CI 安装耗时太久</li>
</ul>
<p>Yarn 就是冲着这些问题来的。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-yarn-的几个关键改进">2. Yarn 的几个关键改进<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#2-yarn-%E7%9A%84%E5%87%A0%E4%B8%AA%E5%85%B3%E9%94%AE%E6%94%B9%E8%BF%9B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<h4 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="并行安装">并行安装<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E5%B9%B6%E8%A1%8C%E5%AE%89%E8%A3%85" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h4>
<p>早期 npm 的安装过程更偏串行，Yarn 把很多步骤并行化了，所以体感速度明显更快。</p>
<h4 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="更好的缓存">更好的缓存<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E6%9B%B4%E5%A5%BD%E7%9A%84%E7%BC%93%E5%AD%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h4>
<p>Yarn 会把下载过的包缓存起来，再次安装时可以更快复用。</p>
<p>这对下面这些场景很有帮助：</p>
<ul>
<li>频繁删除再安装依赖</li>
<li>切分支后重新安装</li>
<li>CI 重复构建</li>
</ul>
<h4 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="yarnlock"><code>yarn.lock</code><a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#yarnlock" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h4>
<p>锁文件的意义很简单：</p>
<p><strong>把“这次安装出来的精确版本”记录下来。</strong></p>
<p>这样团队里每个人、包括 CI，安装结果就更一致，不容易出现：</p>
<p><strong>“为什么你能跑，我这边不行？”</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-yarn-的局限">3. Yarn 的局限<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#3-yarn-%E7%9A%84%E5%B1%80%E9%99%90" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>Yarn 虽然让安装体验更好了，但它并没有彻底解决两个问题：</p>
<ul>
<li><code>node_modules</code> 依然可能很大</li>
<li>依赖管理仍然不够“严格”</li>
</ul>
<p>也就是说，<strong>Yarn 主要解决的是速度和一致性，不是空间复用和依赖隔离。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六pnpm-为什么越来越受欢迎">六、pnpm 为什么越来越受欢迎？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E5%85%ADpnpm-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%B6%8A%E6%9D%A5%E8%B6%8A%E5%8F%97%E6%AC%A2%E8%BF%8E" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果说 npm 和 Yarn 的主要思路是“把依赖装到项目里”，那么 pnpm 更像是在说：</p>
<p><strong>没必要每个项目都存一整份一样的东西。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-pnpm-最核心的优势省空间">1. pnpm 最核心的优势：省空间<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#1-pnpm-%E6%9C%80%E6%A0%B8%E5%BF%83%E7%9A%84%E4%BC%98%E5%8A%BF%E7%9C%81%E7%A9%BA%E9%97%B4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>假设你电脑上有 3 个前端项目，它们都用了 <code>react</code>、<code>typescript</code>、<code>eslint</code>。</p>
<p>用传统方式时，三个项目的 <code>node_modules</code> 里可能各有一份，浪费很明显。</p>
<p>pnpm 的做法是：</p>
<ul>
<li>全局维护一个统一的依赖存储区</li>
<li>相同版本的包只保存一份</li>
<li>各项目通过链接方式使用这些包</li>
</ul>
<p>所以 pnpm 的磁盘占用通常更小。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-pnpm-为什么通常也更快">2. pnpm 为什么通常也更快？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#2-pnpm-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%9A%E5%B8%B8%E4%B9%9F%E6%9B%B4%E5%BF%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>因为它有两层优势：</p>
<ul>
<li>能并行安装</li>
<li>已有包可以直接复用，不用每个项目都重新“存一遍”</li>
</ul>
<p>尤其当你电脑里项目很多时，这种优势会越来越明显。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-pnpm-的另一个关键点依赖更严格">3. pnpm 的另一个关键点：依赖更严格<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#3-pnpm-%E7%9A%84%E5%8F%A6%E4%B8%80%E4%B8%AA%E5%85%B3%E9%94%AE%E7%82%B9%E4%BE%9D%E8%B5%96%E6%9B%B4%E4%B8%A5%E6%A0%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这是很多人第一次接触 pnpm 时最容易踩坑、但也是它最有价值的地方。</p>
<p>先看一个例子。</p>
<p>你的 <code>package.json</code> 里只写了：</p>
<div class="language-json codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-json codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token property" style="color:hsl(5, 74%, 59%)">"dependencies"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token property" style="color:hsl(5, 74%, 59%)">"package-a"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"^1.0.0"</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>但 <code>package-a</code> 自己依赖了 <code>package-b</code>。</p>
<p>在某些旧的依赖结构下，你可能会“误打误撞”直接这样写：</p>
<div class="language-js codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-js codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token keyword module" style="color:hsl(301, 63%, 40%)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token imports"> something </span><span class="token imports punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:hsl(301, 63%, 40%)">from</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">'package-b'</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>虽然你根本没有在自己的 <code>package.json</code> 里声明 <code>package-b</code>，代码却可能还能跑。</p>
<p>这就叫<strong>幽灵依赖</strong>。</p>
<p>问题在于：</p>
<p><strong>它能跑，不代表它对。</strong></p>
<p>一旦换环境、换工具、换版本，就可能突然出问题。</p>
<p>pnpm 更强调“你声明了什么，就只能用什么”，所以它更容易帮你提前发现依赖管理不规范的问题。</p>
<p>这也是为什么很多团队会觉得：</p>
<p><strong>pnpm 一开始更严格，但长期更省心。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七几个容易混淆的概念顺手讲明白">七、几个容易混淆的概念，顺手讲明白<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E4%B8%83%E5%87%A0%E4%B8%AA%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84%E6%A6%82%E5%BF%B5%E9%A1%BA%E6%89%8B%E8%AE%B2%E6%98%8E%E7%99%BD" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-锁文件是什么">1. 锁文件是什么？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#1-%E9%94%81%E6%96%87%E4%BB%B6%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>锁文件就是把依赖安装结果“钉死”。</p>
<p>常见对应关系：</p>
<ul>
<li><code>npm</code> 对应 <code>package-lock.json</code></li>
<li><code>Yarn</code> 对应 <code>yarn.lock</code></li>
<li><code>pnpm</code> 对应 <code>pnpm-lock.yaml</code></li>
</ul>
<p>它的价值主要有两个：</p>
<ul>
<li>团队成员安装结果更一致</li>
<li>CI / 线上环境更稳定</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-什么是幽灵依赖">2. 什么是“幽灵依赖”？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#2-%E4%BB%80%E4%B9%88%E6%98%AF%E5%B9%BD%E7%81%B5%E4%BE%9D%E8%B5%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>一句话解释：</p>
<p><strong>你没声明，但你居然还能用到的依赖。</strong></p>
<p>这不是“方便”，而是“隐患”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-pnpm-里的硬链接--软链接是什么">3. pnpm 里的“硬链接 / 软链接”是什么？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#3-pnpm-%E9%87%8C%E7%9A%84%E7%A1%AC%E9%93%BE%E6%8E%A5--%E8%BD%AF%E9%93%BE%E6%8E%A5%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>如果你不是很关心底层细节，可以只记住这个结论：</p>
<ul>
<li><code>硬链接</code>：多个地方看起来都有这个文件，但底层其实共享同一份内容</li>
<li><code>软链接</code>：像一个指向真实位置的快捷方式</li>
</ul>
<p>pnpm 会利用这些机制，在<strong>兼容 Node.js 模块解析规则</strong>的同时，实现<strong>复用依赖、节省空间、严格隔离</strong>。</p>
<p>你不需要天天研究这些名词，但知道它们是 pnpm 的技术基础就够了。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八三者怎么选这是最实用的部分">八、三者怎么选？这是最实用的部分<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E5%85%AB%E4%B8%89%E8%80%85%E6%80%8E%E4%B9%88%E9%80%89%E8%BF%99%E6%98%AF%E6%9C%80%E5%AE%9E%E7%94%A8%E7%9A%84%E9%83%A8%E5%88%86" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>很多文章讲到最后，读者还是不知道项目里到底该用哪个。这里直接给建议。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-如果你想要最稳妥最省心">1. 如果你想要最稳妥、最省心<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#1-%E5%A6%82%E6%9E%9C%E4%BD%A0%E6%83%B3%E8%A6%81%E6%9C%80%E7%A8%B3%E5%A6%A5%E6%9C%80%E7%9C%81%E5%BF%83" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>选 <code>npm</code>。</p>
<p>原因很简单：</p>
<ul>
<li>Node.js 自带</li>
<li>不需要额外安装</li>
<li>生态兼容性最好</li>
<li>大多数教程默认都能直接跑</li>
</ul>
<p>适合：</p>
<ul>
<li>新手入门</li>
<li>普通前端项目</li>
<li>对团队统一要求不高的项目</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-如果你更在意工程效率">2. 如果你更在意工程效率<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#2-%E5%A6%82%E6%9E%9C%E4%BD%A0%E6%9B%B4%E5%9C%A8%E6%84%8F%E5%B7%A5%E7%A8%8B%E6%95%88%E7%8E%87" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>选 <code>pnpm</code>。</p>
<p>它特别适合：</p>
<ul>
<li>依赖很多的项目</li>
<li>多项目并行开发</li>
<li>Monorepo</li>
<li>希望依赖关系更严格的团队</li>
</ul>
<p>如果你的项目已经进入“工程化”阶段，pnpm 往往是更现代的选择。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-yarn-什么时候会用到">3. Yarn 什么时候会用到？<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#3-yarn-%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E4%BC%9A%E7%94%A8%E5%88%B0" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>主要是两种情况：</p>
<ul>
<li>老项目历史上就在用 Yarn</li>
<li>团队对 Yarn 的工具链已经很熟</li>
</ul>
<p>如果是存量项目，通常没必要为了“追新”强行迁移。</p>
<p>但如果是新项目，今天大多数情况下会更倾向于在 <code>npm</code> 和 <code>pnpm</code> 之间选。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="九一个简单对比表">九、一个简单对比表<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E4%B9%9D%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E5%AF%B9%E6%AF%94%E8%A1%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<table><thead><tr><th>工具</th><th>最大特点</th><th>优点</th><th>可能的不足</th><th>适合谁</th></tr></thead><tbody><tr><td><code>npm</code></td><td>官方默认</td><td>上手简单、兼容性好、生态稳</td><td>大型项目下不一定是最优体验</td><td>大多数普通项目、新手</td></tr><tr><td><code>Yarn</code></td><td>早期以速度和锁文件见长</td><td>体验成熟、老项目常见</td><td>新项目存在感相对下降</td><td>已在用 Yarn 的团队</td></tr><tr><td><code>pnpm</code></td><td>省空间、严格依赖、Monorepo 友好</td><td>快、占用小、依赖更规范</td><td>初次使用时可能会暴露历史依赖问题</td><td>工程化项目、中大型项目</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十把这段历史真正记住只要抓住一条线">十、把这段历史真正记住，只要抓住一条线<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E5%8D%81%E6%8A%8A%E8%BF%99%E6%AE%B5%E5%8E%86%E5%8F%B2%E7%9C%9F%E6%AD%A3%E8%AE%B0%E4%BD%8F%E5%8F%AA%E8%A6%81%E6%8A%93%E4%BD%8F%E4%B8%80%E6%9D%A1%E7%BA%BF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>你可以把它们的演进记成一句话：</p>
<p><strong>npm 先解决“能不能管理包”，Yarn 解决“装得快不快、稳不稳”，pnpm 解决“占不占空间、依赖严不严”。</strong></p>
<p>所以它们的关系不是谁“打败”谁，而是前端工程随着项目规模变大，不断提出新的要求：</p>
<ul>
<li>要更快</li>
<li>要更稳</li>
<li>要更省</li>
<li>要更适合大型项目和 Monorepo</li>
</ul>
<p>这也是前端工具链演进的一个缩影：</p>
<p><strong>先能用，再好用，最后追求更高的工程效率。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十一最后给一个不纠结版本的建议">十一、最后给一个不纠结版本的建议<a href="https://blog.kuizuo.me/blog/npm-yarn-pnpm#%E5%8D%81%E4%B8%80%E6%9C%80%E5%90%8E%E7%BB%99%E4%B8%80%E4%B8%AA%E4%B8%8D%E7%BA%A0%E7%BB%93%E7%89%88%E6%9C%AC%E7%9A%84%E5%BB%BA%E8%AE%AE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果你不想看太多历史，只想知道现在怎么选，可以直接参考：</p>
<ul>
<li>想省事，选 <code>npm</code></li>
<li>想更现代的工程体验，选 <code>pnpm</code></li>
<li>老项目已经在用 <code>Yarn</code>，通常继续用就行</li>
</ul>
<p>对于大部分新项目来说，这已经足够了。</p>
<p>如果以后你再看到这些命令：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">npm</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">install</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">yarn</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">install</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">install</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>你就知道，它们表面都叫“安装依赖”，但背后代表的是三代不同的工程优化思路。</p>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="Node.js" term="Node.js"/>
        <category label="npm" term="npm"/>
        <category label="Yarn" term="Yarn"/>
        <category label="pnpm" term="pnpm"/>
        <category label="包管理" term="包管理"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[pnpm、Changesets、Monorepo 是怎么分工合作的？]]></title>
        <id>https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together</id>
        <link href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together"/>
        <updated>2026-03-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[用通俗的方式讲清 Monorepo、pnpm workspace 和 Changesets 分别负责什么，以及它们在开发和发版流程里是如何配合工作的。]]></summary>
        <content type="html"><![CDATA[<p>很多人第一次接触前端工程化时，看到 <code>Monorepo</code>、<code>pnpm workspace</code>、<code>Changesets</code> 这几个词，经常会有点混：</p>
<ul>
<li>它们是不是一回事？</li>
<li>为什么总是一起出现？</li>
<li>到底谁负责开发，谁负责发布？</li>
</ul>
<p>其实它们不是同一种东西，而是处在<strong>不同层级</strong>。</p>
<p>如果只用一句话总结三者关系，可以这么理解：</p>
<p><strong>Monorepo 负责“组织方式”，pnpm workspace 负责“本地开发和依赖管理”，Changesets 负责“版本管理和发布”。</strong></p>
<p>也就是说：</p>
<ul>
<li><code>Monorepo</code> 是一种架构思路</li>
<li><code>pnpm workspace</code> 是落地这套架构的开发工具</li>
<li><code>Changesets</code> 是给这套架构补上发版流程的工具</li>
</ul>
<p>这篇文章就专门把这三者的分工讲清楚。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一先说结论三者不是同一个层面的东西">一、先说结论：三者不是同一个层面的东西<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E4%B8%80%E5%85%88%E8%AF%B4%E7%BB%93%E8%AE%BA%E4%B8%89%E8%80%85%E4%B8%8D%E6%98%AF%E5%90%8C%E4%B8%80%E4%B8%AA%E5%B1%82%E9%9D%A2%E7%9A%84%E4%B8%9C%E8%A5%BF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>很多人会把这三个词放在一起理解，但其实它们并不在同一层。</p>
<p>你可以先看这张最简化关系图：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">Monorepo：决定“一个仓库放多个包”</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">pnpm workspace：负责识别这些包、安装依赖、管理本地引用</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">Changesets：负责这些包的版本号、CHANGELOG 和发布</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>所以最核心的区别是：</p>
<ul>
<li><code>Monorepo</code> 是<strong>架构模式</strong></li>
<li><code>pnpm workspace</code> 是<strong>开发协作工具</strong></li>
<li><code>Changesets</code> 是<strong>版本与发布工具</strong></li>
</ul>
<p>如果你把它们混成一团，就很容易越学越乱。<br>
<!-- -->但一旦把“层级”分开，事情就会非常清楚。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二monorepo-是什么它负责什么">二、Monorepo 是什么？它负责什么？<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E4%BA%8Cmonorepo-%E6%98%AF%E4%BB%80%E4%B9%88%E5%AE%83%E8%B4%9F%E8%B4%A3%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>先从最上层说。</p>
<p><code>Monorepo</code> 不是一个具体工具，而是一种项目组织方式。它表达的是：</p>
<p><strong>把多个相关项目或多个 npm 包，放进同一个仓库里统一管理。</strong></p>
<p>比如一个仓库可能长这样：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">fe-universal-repo/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">├── apps/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">├── packages/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── ...</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>其中：</p>
<ul>
<li><code>apps</code> 放应用</li>
<li><code>packages</code> 放可复用包</li>
</ul>
<p>这就是 Monorepo 的典型结构。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="monorepo-负责的事情">Monorepo 负责的事情<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#monorepo-%E8%B4%9F%E8%B4%A3%E7%9A%84%E4%BA%8B%E6%83%85" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>它本身只负责回答一个问题：</p>
<p><strong>“这个仓库是不是一个多包仓库？”</strong></p>
<p>也就是说，它决定的是架构方向：</p>
<ul>
<li>一个仓库里能不能放多个包</li>
<li>这些包是不是要一起维护</li>
<li>应用和共享包是不是放在一起</li>
</ul>
<p>但它<strong>不负责具体实现</strong>。</p>
<p>换句话说，Monorepo 更像一种理念，而不是命令行工具。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三pnpm-workspace-是什么它负责什么">三、pnpm workspace 是什么？它负责什么？<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E4%B8%89pnpm-workspace-%E6%98%AF%E4%BB%80%E4%B9%88%E5%AE%83%E8%B4%9F%E8%B4%A3%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果说 Monorepo 是“设计图”，那 <code>pnpm workspace</code> 就是把这张设计图真正落地的工具。</p>
<p>因为光有“一个仓库放多个包”这个想法，还不够。<br>
<!-- -->你还需要解决很多实际问题：</p>
<ul>
<li>哪些目录算包？</li>
<li>包和包之间怎么互相引用？</li>
<li>依赖怎么统一安装？</li>
<li>本地修改后怎么马上生效？</li>
</ul>
<p>这些，都是 <code>pnpm workspace</code> 在做的事。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-它先告诉-pnpm哪些目录是包">1. 它先告诉 pnpm：哪些目录是包<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#1-%E5%AE%83%E5%85%88%E5%91%8A%E8%AF%89-pnpm%E5%93%AA%E4%BA%9B%E7%9B%AE%E5%BD%95%E6%98%AF%E5%8C%85" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这通常通过根目录的 <code>pnpm-workspace.yaml</code> 来声明：</p>
<div class="language-yaml codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-yaml codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token key atrule" style="color:hsl(35, 99%, 36%)">packages</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">-</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"apps/*"</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">-</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"packages/*"</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这段配置的意思很简单：</p>
<ul>
<li><code>apps/*</code> 匹配到的目录，视为 workspace 包</li>
<li><code>packages/*</code> 匹配到的目录，也视为 workspace 包</li>
</ul>
<p>比如：</p>
<ul>
<li><code>apps/workbench</code></li>
<li><code>packages/cli-devkit</code></li>
<li><code>packages/cli-plugin-publish</code></li>
</ul>
<p>都会被 pnpm 当成同一个 workspace 里的成员。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-它负责包之间的本地依赖关系">2. 它负责包之间的本地依赖关系<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#2-%E5%AE%83%E8%B4%9F%E8%B4%A3%E5%8C%85%E4%B9%8B%E9%97%B4%E7%9A%84%E6%9C%AC%E5%9C%B0%E4%BE%9D%E8%B5%96%E5%85%B3%E7%B3%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>假设 <code>cli-plugin-publish</code> 依赖 <code>cli-devkit</code>，那么可以在 <code>package.json</code> 里这样写：</p>
<div class="language-json codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-json codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token property" style="color:hsl(5, 74%, 59%)">"dependencies"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token property" style="color:hsl(5, 74%, 59%)">"@osl/cli-devkit"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"workspace:*"</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这里的 <code>workspace:*</code> 非常关键，它的意思可以简单理解成：</p>
<p><strong>优先使用当前仓库里的本地包，而不是去 npm 上下载。</strong></p>
<p>这带来的效果是：</p>
<ul>
<li>不需要先发布到 npm</li>
<li>包和包之间可以直接联调</li>
<li>改了底层包，上层包通常很快就能看到变化</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-它负责统一安装依赖">3. 它负责统一安装依赖<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#3-%E5%AE%83%E8%B4%9F%E8%B4%A3%E7%BB%9F%E4%B8%80%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>在 workspace 根目录执行：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">install</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>pnpm 会做的不只是当前目录安装，而是：</p>
<ul>
<li>扫描整个 workspace</li>
<li>安装所有包的依赖</li>
<li>建立包之间的本地引用关系</li>
<li>尽量复用依赖，减少重复安装</li>
</ul>
<p>所以 pnpm workspace 的核心职责，概括起来就是：</p>
<p><strong>让 Monorepo 里的多个包能在本地顺畅协作。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四changesets-是什么它负责什么">四、Changesets 是什么？它负责什么？<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E5%9B%9Bchangesets-%E6%98%AF%E4%BB%80%E4%B9%88%E5%AE%83%E8%B4%9F%E8%B4%A3%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>当你已经用 Monorepo + workspace 把开发阶段跑顺之后，下一个问题就是：</p>
<p><strong>这些包要怎么发版？</strong></p>
<p>这时候 Changesets 就上场了。</p>
<p>它主要解决的不是“怎么开发”，而是：</p>
<ul>
<li>哪个包要升级版本</li>
<li>升 <code>patch</code>、<code>minor</code> 还是 <code>major</code></li>
<li>CHANGELOG 怎么生成</li>
<li>内部依赖包版本怎么联动</li>
<li>最后怎么把这些包发布出去</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-它先记录变更">1. 它先记录变更<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#1-%E5%AE%83%E5%85%88%E8%AE%B0%E5%BD%95%E5%8F%98%E6%9B%B4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>每次完成一项改动后，可以创建一个 changeset：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">npx @osl/cli changeset </span><span class="token function" style="color:hsl(221, 87%, 60%)">add</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>它会生成一个 <code>.changeset/xxx.md</code> 文件，用来记录：</p>
<ul>
<li>哪个包要发版</li>
<li>版本类型是什么</li>
<li>这次改了什么</li>
</ul>
<p>你可以把它理解成一张“发版记录单”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-它统一计算版本号">2. 它统一计算版本号<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#2-%E5%AE%83%E7%BB%9F%E4%B8%80%E8%AE%A1%E7%AE%97%E7%89%88%E6%9C%AC%E5%8F%B7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>准备发版时，再执行：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">npx @osl/cli changeset version</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这一步会：</p>
<ul>
<li>读取所有 changeset 文件</li>
<li>计算每个包的新版本</li>
<li>更新 <code>package.json</code></li>
<li>生成或更新 <code>CHANGELOG.md</code></li>
<li>处理内部依赖包的版本联动</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-它配合发布脚本完成真正的发布">3. 它配合发布脚本完成真正的发布<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#3-%E5%AE%83%E9%85%8D%E5%90%88%E5%8F%91%E5%B8%83%E8%84%9A%E6%9C%AC%E5%AE%8C%E6%88%90%E7%9C%9F%E6%AD%A3%E7%9A%84%E5%8F%91%E5%B8%83" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>例如：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> publish:local</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>在项目配置好的前提下，这一步通常会：</p>
<ul>
<li>构建包</li>
<li>发布到 npm registry</li>
<li>打 git tag</li>
</ul>
<p>所以 Changesets 的职责很明确：</p>
<p><strong>它主要负责发版前后的版本管理，而不是取代 pnpm 来管理依赖。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五三者在开发阶段是怎么配合的">五、三者在开发阶段是怎么配合的？<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E4%BA%94%E4%B8%89%E8%80%85%E5%9C%A8%E5%BC%80%E5%8F%91%E9%98%B6%E6%AE%B5%E6%98%AF%E6%80%8E%E4%B9%88%E9%85%8D%E5%90%88%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>先看开发阶段。</p>
<p>假设现在有两个包：</p>
<ul>
<li><code>cli-devkit</code></li>
<li><code>cli-plugin-publish</code></li>
</ul>
<p>其中 <code>cli-plugin-publish</code> 依赖 <code>cli-devkit</code>。</p>
<p>在开发阶段，三者分工可以这样理解：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-monorepo-提供整体结构">1. Monorepo 提供整体结构<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#1-monorepo-%E6%8F%90%E4%BE%9B%E6%95%B4%E4%BD%93%E7%BB%93%E6%9E%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>它决定了这两个包放在同一个仓库里维护。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-pnpm-workspace-负责本地协作">2. pnpm workspace 负责本地协作<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#2-pnpm-workspace-%E8%B4%9F%E8%B4%A3%E6%9C%AC%E5%9C%B0%E5%8D%8F%E4%BD%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>它让两个包可以通过 <code>workspace:*</code> 直接引用。</p>
<p>所以开发者在本地调试时会有一个非常明显的体验：</p>
<ul>
<li>修改 <code>cli-devkit</code></li>
<li><code>cli-plugin-publish</code> 很快就能用到新代码</li>
<li>不需要先发布到 npm</li>
<li>不需要来回安装远程依赖</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-changesets-在这个阶段先不发版只负责记账">3. Changesets 在这个阶段先不发版，只负责“记账”<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#3-changesets-%E5%9C%A8%E8%BF%99%E4%B8%AA%E9%98%B6%E6%AE%B5%E5%85%88%E4%B8%8D%E5%8F%91%E7%89%88%E5%8F%AA%E8%B4%9F%E8%B4%A3%E8%AE%B0%E8%B4%A6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>也就是说，Changesets 在开发阶段不是主角。<br>
<!-- -->它做的是在你完成改动后，帮你把“将来这次要怎么发”先记录下来。</p>
<p>所以开发阶段最主要的配合关系是：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">Monorepo 决定把多个包放一起</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">pnpm workspace 让这些包能本地协作</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">Changesets 记录这次改动将来怎么发版</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六三者在发布阶段是怎么配合的">六、三者在发布阶段是怎么配合的？<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E5%85%AD%E4%B8%89%E8%80%85%E5%9C%A8%E5%8F%91%E5%B8%83%E9%98%B6%E6%AE%B5%E6%98%AF%E6%80%8E%E4%B9%88%E9%85%8D%E5%90%88%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>到了发版阶段，主角就从 <code>pnpm workspace</code> 变成了 <code>Changesets</code>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-changesets-先读取变更记录">1. Changesets 先读取变更记录<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#1-changesets-%E5%85%88%E8%AF%BB%E5%8F%96%E5%8F%98%E6%9B%B4%E8%AE%B0%E5%BD%95" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>它会把之前开发过程中积累的 <code>.changeset/*.md</code> 文件全部读出来。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-根据包结构判断影响范围">2. 根据包结构判断影响范围<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#2-%E6%A0%B9%E6%8D%AE%E5%8C%85%E7%BB%93%E6%9E%84%E5%88%A4%E6%96%AD%E5%BD%B1%E5%93%8D%E8%8C%83%E5%9B%B4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这一步并不是凭空算，而是结合 workspace 里的包关系来处理。</p>
<p>也就是说，Changesets 之所以知道“哪些包会受影响”，是因为仓库本来就已经通过 workspace 建立了多包结构和依赖关系。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-统一更新版本changelog-和内部依赖">3. 统一更新版本、CHANGELOG 和内部依赖<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#3-%E7%BB%9F%E4%B8%80%E6%9B%B4%E6%96%B0%E7%89%88%E6%9C%ACchangelog-%E5%92%8C%E5%86%85%E9%83%A8%E4%BE%9D%E8%B5%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>比如：</p>
<ul>
<li><code>cli-devkit</code> 升级了</li>
<li><code>cli-plugin-publish</code> 依赖它</li>
</ul>
<p>那 Changesets 就可以根据配置去联动更新上层包版本。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-最后交给发布命令真正发出去">4. 最后交给发布命令真正发出去<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#4-%E6%9C%80%E5%90%8E%E4%BA%A4%E7%BB%99%E5%8F%91%E5%B8%83%E5%91%BD%E4%BB%A4%E7%9C%9F%E6%AD%A3%E5%8F%91%E5%87%BA%E5%8E%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这个阶段通常还是借助 <code>pnpm</code> 的脚本体系来执行构建和发布。</p>
<p>所以发布阶段可以总结成：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">Changesets 读取变更记录</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">结合 workspace 里的包关系</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">计算版本号和 changelog</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">配合 pnpm 脚本完成发布</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七用一个完整例子串起来看">七、用一个完整例子串起来看<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E4%B8%83%E7%94%A8%E4%B8%80%E4%B8%AA%E5%AE%8C%E6%95%B4%E4%BE%8B%E5%AD%90%E4%B8%B2%E8%B5%B7%E6%9D%A5%E7%9C%8B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>假设你修复了 <code>cli-plugin-publish</code> 的一个 bug。</p>
<p>那么整个流程大概是这样：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第一步在-monorepo-里开发">第一步：在 Monorepo 里开发<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E7%AC%AC%E4%B8%80%E6%AD%A5%E5%9C%A8-monorepo-%E9%87%8C%E5%BC%80%E5%8F%91" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>你在同一个仓库里修改代码，比如：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">packages/cli-plugin-publish/src/lib/utils.ts</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>如果这个包依赖了 <code>cli-devkit</code>，那么因为有 workspace，本地依赖关系已经打通，不需要先发 npm 包再测试。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第二步用-changesets-记录这次改动">第二步：用 Changesets 记录这次改动<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E7%AC%AC%E4%BA%8C%E6%AD%A5%E7%94%A8-changesets-%E8%AE%B0%E5%BD%95%E8%BF%99%E6%AC%A1%E6%94%B9%E5%8A%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">npx @osl/cli changeset </span><span class="token function" style="color:hsl(221, 87%, 60%)">add</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这一步会记录：</p>
<ul>
<li>改的是哪个包</li>
<li>是 <code>patch</code>、<code>minor</code> 还是 <code>major</code></li>
<li>这次改动的描述</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第三步准备发版时统一升级版本">第三步：准备发版时统一升级版本<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E7%AC%AC%E4%B8%89%E6%AD%A5%E5%87%86%E5%A4%87%E5%8F%91%E7%89%88%E6%97%B6%E7%BB%9F%E4%B8%80%E5%8D%87%E7%BA%A7%E7%89%88%E6%9C%AC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">npx @osl/cli changeset version</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>它会自动：</p>
<ul>
<li>计算版本号</li>
<li>更新 <code>package.json</code></li>
<li>更新 <code>CHANGELOG.md</code></li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第四步执行发布">第四步：执行发布<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E7%AC%AC%E5%9B%9B%E6%AD%A5%E6%89%A7%E8%A1%8C%E5%8F%91%E5%B8%83" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">pnpm</span><span class="token plain"> publish:local</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这一步把最终的构建和发包真正跑起来。</p>
<p>所以这个完整流程，其实就是：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">Monorepo 提供多包仓库结构</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">pnpm workspace 负责本地开发协作</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">Changesets 负责版本记录和发布准备</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">pnpm 脚本执行最终发布</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八再用一个更容易记住的类比">八、再用一个更容易记住的类比<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E5%85%AB%E5%86%8D%E7%94%A8%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%AE%B9%E6%98%93%E8%AE%B0%E4%BD%8F%E7%9A%84%E7%B1%BB%E6%AF%94" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果还是觉得抽象，可以把它们类比成“建房子”：</p>
<ul>
<li><code>Monorepo</code> = 房子的设计方案</li>
<li><code>pnpm workspace</code> = 施工和日常管理工具</li>
<li><code>Changesets</code> = 交房时的登记、编号和销售流程</li>
</ul>
<p>这个类比不一定完全严谨，但对初学者很好记。</p>
<p>因为它刚好对应三者的职责：</p>
<ul>
<li><code>Monorepo</code> 决定整体结构</li>
<li><code>pnpm workspace</code> 让结构真正运行起来</li>
<li><code>Changesets</code> 让最终版本和发布流程可控</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="九最容易混淆的地方我再帮你拆一下">九、最容易混淆的地方，我再帮你拆一下<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E4%B9%9D%E6%9C%80%E5%AE%B9%E6%98%93%E6%B7%B7%E6%B7%86%E7%9A%84%E5%9C%B0%E6%96%B9%E6%88%91%E5%86%8D%E5%B8%AE%E4%BD%A0%E6%8B%86%E4%B8%80%E4%B8%8B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-monorepo-不是-pnpm">1. Monorepo 不是 pnpm<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#1-monorepo-%E4%B8%8D%E6%98%AF-pnpm" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>Monorepo 是一种架构思路，不是某个命令行工具。</p>
<p>你可以用 pnpm 来做 Monorepo，也可以用其他工具链来做。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-pnpm-workspace-不负责版本发布策略">2. pnpm workspace 不负责版本发布策略<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#2-pnpm-workspace-%E4%B8%8D%E8%B4%9F%E8%B4%A3%E7%89%88%E6%9C%AC%E5%8F%91%E5%B8%83%E7%AD%96%E7%95%A5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>pnpm workspace 主要负责包管理、依赖安装、本地联动。<br>
<!-- -->它不是专门解决 CHANGELOG 和多包发版规则的工具。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-changesets-不负责创建-monorepo">3. Changesets 不负责创建 Monorepo<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#3-changesets-%E4%B8%8D%E8%B4%9F%E8%B4%A3%E5%88%9B%E5%BB%BA-monorepo" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>Changesets 假设你的多包结构已经存在，它是在这个基础上补上版本和发版管理。</p>
<p>所以更准确的理解是：</p>
<p><strong>Changesets 是建立在 Monorepo + workspace 之上的发版层。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十最后怎么记住三者的分工">十、最后怎么记住三者的分工？<a href="https://blog.kuizuo.me/blog/pnpm-changesets-monorepo-how-they-work-together#%E5%8D%81%E6%9C%80%E5%90%8E%E6%80%8E%E4%B9%88%E8%AE%B0%E4%BD%8F%E4%B8%89%E8%80%85%E7%9A%84%E5%88%86%E5%B7%A5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果你只想记一句最重要的话，可以记这个版本：</p>
<p><strong>Monorepo 决定“把多个包放在一个仓库里”，pnpm workspace 解决“这些包怎么在本地协作”，Changesets 解决“这些包怎么统一发版”。</strong></p>
<p>再压缩一点就是：</p>
<ul>
<li><code>Monorepo</code>：管结构</li>
<li><code>pnpm workspace</code>：管开发</li>
<li><code>Changesets</code>：管发布</li>
</ul>
<p>这三者不是互相替代，而是刚好拼成了一套完整的多包工程化工作流。</p>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="pnpm" term="pnpm"/>
        <category label="Changesets" term="Changesets"/>
        <category label="Monorepo" term="Monorepo"/>
        <category label="Workspace" term="Workspace"/>
        <category label="前端工程化" term="前端工程化"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[pnpm 底层到底做了什么？一篇讲清内容寻址、硬链接和软链接]]></title>
        <id>https://blog.kuizuo.me/blog/how-pnpm-works</id>
        <link href="https://blog.kuizuo.me/blog/how-pnpm-works"/>
        <updated>2026-03-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[用通俗的方式讲清 pnpm 的底层原理：为什么它更省空间、更快，以及它是如何通过内容寻址、硬链接和软链接管理依赖的。]]></summary>
        <content type="html"><![CDATA[<p>很多人第一次用 <code>pnpm</code>，都会先感受到两个特点：</p>
<ul>
<li>安装速度挺快</li>
<li><code>node_modules</code> 看起来和 <code>npm</code>、<code>Yarn</code> 不太一样</li>
</ul>
<p>然后就会冒出一个问题：</p>
<p><strong>pnpm 到底做了什么，为什么它既能省空间，又能保持依赖可用？</strong></p>
<p>如果你去搜它的原理，常常会看到一堆词：</p>
<ul>
<li>内容寻址存储</li>
<li>硬链接</li>
<li>软链接</li>
<li>严格依赖隔离</li>
</ul>
<p>这些词本身没错，但一上来全堆出来，反而很容易越看越晕。</p>
<p>这篇文章我想用更通俗的方式，把 pnpm 的底层逻辑讲清楚。你不需要先懂文件系统，只要抓住一条主线：</p>
<p><strong>pnpm 的核心目标，就是“同样的包尽量只存一份，同时又要让 Node.js 还能正常找到它”。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一先说结论pnpm-为什么和别的包管理器不一样">一、先说结论：pnpm 为什么和别的包管理器不一样？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E4%B8%80%E5%85%88%E8%AF%B4%E7%BB%93%E8%AE%BApnpm-%E4%B8%BA%E4%BB%80%E4%B9%88%E5%92%8C%E5%88%AB%E7%9A%84%E5%8C%85%E7%AE%A1%E7%90%86%E5%99%A8%E4%B8%8D%E4%B8%80%E6%A0%B7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>一句话概括：</p>
<p><strong>npm / Yarn 更像是把依赖“搬进你的项目”，pnpm 更像是“全局存一份，项目里用链接接过去”。</strong></p>
<p>这就是区别的根源。</p>
<p>也正因为这样，pnpm 往往会有几个明显优势：</p>
<ul>
<li>更省磁盘空间</li>
<li>多项目场景下复用率更高</li>
<li>安装通常更快</li>
<li>依赖关系更严格，不容易出现“幽灵依赖”</li>
</ul>
<p>但它不是魔法，它背后主要依赖三件事：</p>
<ol>
<li>内容寻址存储</li>
<li>硬链接</li>
<li>软链接</li>
</ol>
<p>下面我们一个个拆开。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二先理解问题为什么传统-node_modules-会很浪费">二、先理解问题：为什么传统 <code>node_modules</code> 会很浪费？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E4%BA%8C%E5%85%88%E7%90%86%E8%A7%A3%E9%97%AE%E9%A2%98%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%A0%E7%BB%9F-node_modules-%E4%BC%9A%E5%BE%88%E6%B5%AA%E8%B4%B9" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>假设你电脑里有 3 个项目：</p>
<ul>
<li><code>project-a</code></li>
<li><code>project-b</code></li>
<li><code>project-c</code></li>
</ul>
<p>它们都安装了 <code>lodash@4.17.21</code>。</p>
<p>传统思路通常是：</p>
<p><strong>每个项目自己的 <code>node_modules</code> 都放一份 <code>lodash</code>。</strong></p>
<p>看起来没问题，但实际上这三份文件几乎一模一样。</p>
<p>于是就出现了浪费：</p>
<ul>
<li>占用重复磁盘空间</li>
<li>装依赖时重复写入大量文件</li>
<li>项目越多，浪费越明显</li>
</ul>
<p>pnpm 看到这个问题后的想法其实很朴素：</p>
<p><strong>既然内容一样，为什么不只保存一份？</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三pnpm-的第一步把包放进一个总仓库">三、pnpm 的第一步：把包放进一个“总仓库”<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E4%B8%89pnpm-%E7%9A%84%E7%AC%AC%E4%B8%80%E6%AD%A5%E6%8A%8A%E5%8C%85%E6%94%BE%E8%BF%9B%E4%B8%80%E4%B8%AA%E6%80%BB%E4%BB%93%E5%BA%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>pnpm 不会一上来就把每个包完整复制到项目里，而是先放进一个全局存储区，通常叫做 <code>store</code>。</p>
<p>你可以把它理解成一个总仓库：</p>
<ul>
<li>相同内容的文件，全局尽量只存一份</li>
<li>各个项目需要时，再通过链接使用它</li>
</ul>
<p>这个目录一般长这样：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">~/.pnpm-store/</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>所以 pnpm 的思路不是：</p>
<p><strong>项目 A 一份，项目 B 一份，项目 C 再来一份</strong></p>
<p>而是：</p>
<p><strong>先在总仓库里存好，再让不同项目复用。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四什么叫内容寻址存储">四、什么叫“内容寻址存储”？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E5%9B%9B%E4%BB%80%E4%B9%88%E5%8F%AB%E5%86%85%E5%AE%B9%E5%AF%BB%E5%9D%80%E5%AD%98%E5%82%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这个词听起来很硬，其实意思不复杂。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-传统方式按名字找">1. 传统方式：按名字找<a href="https://blog.kuizuo.me/blog/how-pnpm-works#1-%E4%BC%A0%E7%BB%9F%E6%96%B9%E5%BC%8F%E6%8C%89%E5%90%8D%E5%AD%97%E6%89%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>传统存储更像这样：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">~/.npm/package-name@1.0.0/</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>也就是根据：</p>
<ul>
<li>包名</li>
<li>版本号</li>
</ul>
<p>来决定它放在哪。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-pnpm-的方式按内容找">2. pnpm 的方式：按内容找<a href="https://blog.kuizuo.me/blog/how-pnpm-works#2-pnpm-%E7%9A%84%E6%96%B9%E5%BC%8F%E6%8C%89%E5%86%85%E5%AE%B9%E6%89%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>pnpm 更强调：</p>
<p><strong>不是看你叫什么名字，而是看你内容是不是一样。</strong></p>
<p>如果两个文件内容完全相同，那么它们没有必要存两份。</p>
<p>所以 pnpm 会根据文件内容的哈希值来定位文件，类似这样：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">~/.pnpm-store/v3/files/00/abc123...</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这就叫 <strong>内容寻址存储（Content-addressable Store）</strong>。</p>
<p>这样做的好处是：</p>
<ul>
<li>相同内容只需要存一份</li>
<li>更容易复用</li>
<li>还能顺带做内容校验，确保文件没损坏</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五光有总仓库还不够项目怎么用这些包">五、光有总仓库还不够，项目怎么用这些包？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E4%BA%94%E5%85%89%E6%9C%89%E6%80%BB%E4%BB%93%E5%BA%93%E8%BF%98%E4%B8%8D%E5%A4%9F%E9%A1%B9%E7%9B%AE%E6%80%8E%E4%B9%88%E7%94%A8%E8%BF%99%E4%BA%9B%E5%8C%85" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这就轮到 <strong>硬链接</strong> 出场了。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-硬链接是什么">1. 硬链接是什么？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#1-%E7%A1%AC%E9%93%BE%E6%8E%A5%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>你可以把硬链接理解成：</p>
<p><strong>同一份文件内容，有多个“入口”。</strong></p>
<p>它不是“复制出一个新文件”，而是让不同路径都指向同一份实际数据。</p>
<p>比如：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">cp</span><span class="token plain"> file.txt copy.txt</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这是复制，结果是两份独立内容。</p>
<p>而：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token function" style="color:hsl(221, 87%, 60%)">ln</span><span class="token plain"> file.txt link.txt</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这是创建硬链接，结果看起来像两个文件，底层其实共享同一份数据。</p>
<p>所以核心区别就一句话：</p>
<ul>
<li>复制：占两份空间</li>
<li>硬链接：看起来有两个入口，实际上只占一份空间</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-pnpm-怎么用硬链接">2. pnpm 怎么用硬链接？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#2-pnpm-%E6%80%8E%E4%B9%88%E7%94%A8%E7%A1%AC%E9%93%BE%E6%8E%A5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>pnpm 会把 store 里的文件，通过硬链接放到项目内部的 <code>.pnpm</code> 目录里。</p>
<p>大致可以理解成这样：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">~/.pnpm-store/v3/files/...</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">        ↑</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">        │ 硬链接</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">        │</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">project/node_modules/.pnpm/package-a@1.0.0/node_modules/package-a/</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这意味着：</p>
<ul>
<li>项目里看起来“有这个包”</li>
<li>但底层数据并不是重新复制出来的</li>
<li>多个项目可以共享同一份文件内容</li>
</ul>
<p>这也是 pnpm 省空间的关键原因之一。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六那为什么还要软链接">六、那为什么还要软链接？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E5%85%AD%E9%82%A3%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%98%E8%A6%81%E8%BD%AF%E9%93%BE%E6%8E%A5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>看到这里很多人会问：</p>
<p><strong>既然 <code>.pnpm</code> 里已经有包了，为什么还要再搞一层软链接？</strong></p>
<p>答案是：</p>
<p><strong>因为 Node.js 找包的方式，默认就是从 <code>node_modules/package-name</code> 这个路径往下找。</strong></p>
<p>也就是说，Node.js 并不会天然跑去认 <code>.pnpm/package-a@1.0.0/...</code> 这种内部结构。</p>
<p>所以 pnpm 需要再做一层“门面”，让 Node.js 看起来依然像在用熟悉的 <code>node_modules</code>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-软链接是什么">1. 软链接是什么？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#1-%E8%BD%AF%E9%93%BE%E6%8E%A5%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>软链接可以理解成一个“快捷方式”。</p>
<p>它本身不是实际文件内容，只是指向另一个路径。</p>
<p>比如：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">node_modules/package-a</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这个位置看起来像真的有个包，但它实际上可能只是一个软链接，指向：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">node_modules/.pnpm/package-a@1.0.0/node_modules/package-a</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-pnpm-的完整链路">2. pnpm 的完整链路<a href="https://blog.kuizuo.me/blog/how-pnpm-works#2-pnpm-%E7%9A%84%E5%AE%8C%E6%95%B4%E9%93%BE%E8%B7%AF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>所以一个包在 pnpm 里，通常会形成这样的关系：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">node_modules/package-a</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  ↓ 软链接</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">node_modules/.pnpm/package-a@1.0.0/node_modules/package-a</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  ↓ 硬链接</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">~/.pnpm-store/v3/files/...</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>把它翻译成人话就是：</p>
<ul>
<li>最底层真实文件放在全局 store</li>
<li>项目内部 <code>.pnpm</code> 目录通过硬链接复用这些文件</li>
<li>顶层 <code>node_modules/package-a</code> 再通过软链接暴露给 Node.js 使用</li>
</ul>
<p>这就是 pnpm 为什么经常会被说成是“两层链接结构”。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七为什么必须是两层链接不能只用一层">七、为什么必须是“两层链接”，不能只用一层？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E4%B8%83%E4%B8%BA%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E6%98%AF%E4%B8%A4%E5%B1%82%E9%93%BE%E6%8E%A5%E4%B8%8D%E8%83%BD%E5%8F%AA%E7%94%A8%E4%B8%80%E5%B1%82" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这个问题很关键。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-第一层硬链接解决的是省空间">1. 第一层硬链接，解决的是“省空间”<a href="https://blog.kuizuo.me/blog/how-pnpm-works#1-%E7%AC%AC%E4%B8%80%E5%B1%82%E7%A1%AC%E9%93%BE%E6%8E%A5%E8%A7%A3%E5%86%B3%E7%9A%84%E6%98%AF%E7%9C%81%E7%A9%BA%E9%97%B4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p><code>.pnpm</code> 到 store 之间使用硬链接，目的很明确：</p>
<ul>
<li>复用已有文件</li>
<li>不重复占用空间</li>
<li>多个项目共享同一份内容</li>
</ul>
<p>也就是说，硬链接主要解决的是 <strong>效率和存储成本</strong>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-第二层软链接解决的是兼容-nodejs">2. 第二层软链接，解决的是“兼容 Node.js”<a href="https://blog.kuizuo.me/blog/how-pnpm-works#2-%E7%AC%AC%E4%BA%8C%E5%B1%82%E8%BD%AF%E9%93%BE%E6%8E%A5%E8%A7%A3%E5%86%B3%E7%9A%84%E6%98%AF%E5%85%BC%E5%AE%B9-nodejs" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>顶层 <code>node_modules/package-a</code> 到 <code>.pnpm/.../package-a</code> 之间使用软链接，目的则是：</p>
<ul>
<li>保持 Node.js 模块解析兼容</li>
<li>让 <code>require('package-a')</code> / <code>import 'package-a'</code> 正常工作</li>
</ul>
<p>如果没有这层，Node.js 找包时会很别扭，很多现有工具链也可能不兼容。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-两层组合起来还能顺手实现依赖隔离">3. 两层组合起来，还能顺手实现“依赖隔离”<a href="https://blog.kuizuo.me/blog/how-pnpm-works#3-%E4%B8%A4%E5%B1%82%E7%BB%84%E5%90%88%E8%B5%B7%E6%9D%A5%E8%BF%98%E8%83%BD%E9%A1%BA%E6%89%8B%E5%AE%9E%E7%8E%B0%E4%BE%9D%E8%B5%96%E9%9A%94%E7%A6%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这是 pnpm 很重要但经常被忽视的一点。</p>
<p>pnpm 不只是为了省空间，它还希望做到：</p>
<p><strong>一个包只能访问它真正声明过的依赖。</strong></p>
<p>比如 <code>package-a</code> 依赖了 <code>package-b</code>，那么在 pnpm 的结构下，<code>package-a</code> 会在属于自己的那层依赖目录里访问到 <code>package-b</code>。</p>
<p>这样做的结果是：</p>
<ul>
<li>依赖关系更清晰</li>
<li>不容易误用没声明的包</li>
<li>能减少“幽灵依赖”</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-用一句更准确的人话总结">4. 用一句更准确的人话总结<a href="https://blog.kuizuo.me/blog/how-pnpm-works#4-%E7%94%A8%E4%B8%80%E5%8F%A5%E6%9B%B4%E5%87%86%E7%A1%AE%E7%9A%84%E4%BA%BA%E8%AF%9D%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>如果用最通俗的话来说，你的理解方向是对的，但可以再修正得更准确一点：</p>
<ul>
<li><strong>软链接更像“地址”或“快捷方式”</strong>，它指向 <code>.pnpm</code> 目录里的实际包位置，主要作用是让 Node.js 还能按熟悉的 <code>node_modules/package-name</code> 路径去找模块。</li>
<li><strong>硬链接不是“文件内容本身”，更准确地说，它是同一份文件数据的另一个入口。</strong></li>
<li><strong>多个硬链接看起来像是多个文件路径，但底层其实共享同一份真实数据</strong>，所以不会重复占用同样的磁盘空间。</li>
</ul>
<p>如果把这件事压缩成一句话，可以理解为：</p>
<p><strong>软链接负责“让 Node.js 找得到”，硬链接负责“让多个位置共用同一份文件”。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八什么是幽灵依赖pnpm-为什么能减少它">八、什么是“幽灵依赖”？pnpm 为什么能减少它？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E5%85%AB%E4%BB%80%E4%B9%88%E6%98%AF%E5%B9%BD%E7%81%B5%E4%BE%9D%E8%B5%96pnpm-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%83%BD%E5%87%8F%E5%B0%91%E5%AE%83" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>先说最短定义：</p>
<p><strong>幽灵依赖（Phantom Dependency）就是：你没有在自己的 <code>package.json</code> 里声明某个包，但代码里却能直接用它。</strong></p>
<p>这类问题最危险的地方在于，它常常不是“立刻报错”，而是：</p>
<ul>
<li>在你电脑上能跑</li>
<li>换一台机器可能不能跑</li>
<li>升级依赖后突然报错</li>
<li>CI 和本地结果不一致</li>
</ul>
<p>所以它不是“方便”，而是一个隐藏得很深的坑。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-幽灵依赖是怎么出现的">1. 幽灵依赖是怎么出现的？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#1-%E5%B9%BD%E7%81%B5%E4%BE%9D%E8%B5%96%E6%98%AF%E6%80%8E%E4%B9%88%E5%87%BA%E7%8E%B0%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>假设你的项目只安装了 <code>package-a</code>：</p>
<div class="language-json codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-json codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token property" style="color:hsl(5, 74%, 59%)">"dependencies"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token property" style="color:hsl(5, 74%, 59%)">"package-a"</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">"^1.0.0"</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>而 <code>package-a</code> 自己依赖了 <code>package-b</code>。</p>
<p>按道理讲，你的项目并没有直接安装 <code>package-b</code>，所以你不应该直接这样写：</p>
<div class="language-js codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-js codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token keyword module" style="color:hsl(301, 63%, 40%)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token imports"> something </span><span class="token imports punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:hsl(301, 63%, 40%)">from</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">'package-b'</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>但在一些传统的 <code>node_modules</code> 结构里，这段代码却可能“误打误撞”运行成功。</p>
<p>为什么？因为很多包管理器会把依赖尽量<strong>扁平化</strong>到顶层 <code>node_modules</code>。</p>
<p>比如可能会变成这样：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">node_modules/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">├── package-a/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">├── package-b/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── ...</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>虽然你的项目只声明了 <code>package-a</code>，但因为 <code>package-b</code> 被提升到了顶层，Node.js 依然能在 <code>node_modules/package-b</code> 这里找到它。</p>
<p>于是就出现了一个很迷惑的现象：</p>
<ul>
<li>你没声明它</li>
<li>但它刚好在顶层</li>
<li>Node.js 又确实能找到</li>
<li>所以代码表面上“没问题”</li>
</ul>
<p>这就是幽灵依赖产生的根源。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-为什么这会成为问题">2. 为什么这会成为问题？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#2-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%99%E4%BC%9A%E6%88%90%E4%B8%BA%E9%97%AE%E9%A2%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>因为这会让“代码实际能访问到的依赖”和“你明确声明的依赖”不是一回事。</p>
<p>表面上看，项目跑起来了；但实际上，它偷偷依赖了某个你没有显式安装的包。</p>
<p>这会带来几个后果：</p>
<ul>
<li>新同事拉代码后环境不一定一致</li>
<li>升级 <code>package-a</code> 时，<code>package-b</code> 可能被移除或换版本</li>
<li>包管理器一变，原来“能跑”的代码就不跑了</li>
<li>你很难从 <code>package.json</code> 一眼看出项目真实依赖了什么</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-pnpm-是怎么控制这个问题的">3. pnpm 是怎么控制这个问题的？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#3-pnpm-%E6%98%AF%E6%80%8E%E4%B9%88%E6%8E%A7%E5%88%B6%E8%BF%99%E4%B8%AA%E9%97%AE%E9%A2%98%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>pnpm 的思路不是把所有依赖都暴露到项目顶层，而是：</p>
<p><strong>谁声明了谁，谁就只能在自己的依赖上下文里访问它。</strong></p>
<p>还是上面的例子：</p>
<ul>
<li>你的项目依赖 <code>package-a</code></li>
<li><code>package-a</code> 依赖 <code>package-b</code></li>
</ul>
<p>在 pnpm 里，更接近下面这种结构：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">node_modules/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">├── package-a -&gt; .pnpm/package-a@1.0.0/node_modules/package-a</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── .pnpm/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ├── package-a@1.0.0/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    │   └── node_modules/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    │       ├── package-a/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    │       └── package-b/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    └── package-b@1.0.0/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">        └── node_modules/</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">            └── package-b/</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这时候最关键的一点是：</p>
<p><strong><code>package-b</code> 是放在 <code>package-a</code> 自己那层依赖环境里的，不是随便平铺在你的项目顶层入口上。</strong></p>
<p>于是访问关系就变得很清楚：</p>
<ul>
<li><code>package-a</code> 可以访问 <code>package-b</code></li>
<li>你的应用代码只能直接访问你自己声明过的依赖</li>
<li>如果你没有声明 <code>package-b</code>，通常就不能理直气壮地直接 <code>import 'package-b'</code></li>
</ul>
<p>这就是 pnpm 的“依赖隔离”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-pnpm-的依赖隔离隔离的到底是什么">4. pnpm 的依赖隔离，隔离的到底是什么？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#4-pnpm-%E7%9A%84%E4%BE%9D%E8%B5%96%E9%9A%94%E7%A6%BB%E9%9A%94%E7%A6%BB%E7%9A%84%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>它隔离的不是“文件不能共享”，而是：</p>
<p><strong>不同层级的代码，只能访问属于自己那一层的依赖集合。</strong></p>
<p>换句话说：</p>
<ul>
<li>你的应用有自己的直接依赖</li>
<li><code>package-a</code> 也有它自己的直接依赖</li>
<li><code>package-b</code> 也有它自己的直接依赖</li>
</ul>
<p>pnpm 会尽量让这些依赖关系按声明组织起来，而不是全部摊平成一个大杂烩。</p>
<p>所以它实现的其实是：</p>
<p><strong>让真实可访问的依赖，尽量等于声明过的依赖。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-为什么说-pnpm-是更严格">5. 为什么说 pnpm 是“更严格”？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#5-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AF%B4-pnpm-%E6%98%AF%E6%9B%B4%E4%B8%A5%E6%A0%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>因为它会更早暴露问题。</p>
<p>以前在 npm / Yarn 某些扁平化场景下，你可能写了不规范代码也能跑；换到 pnpm 后，代码很可能立刻就报找不到模块。</p>
<p>这听起来像“pnpm 更麻烦”，但本质上它只是帮你把原来被掩盖的问题提前暴露出来了。</p>
<p>从长期看，这反而是好事，因为：</p>
<ul>
<li>依赖关系更清晰</li>
<li>团队协作更稳定</li>
<li>CI 和本地更一致</li>
<li>升级依赖时更不容易踩隐藏坑</li>
</ul>
<p>所以更准确地说，pnpm 不是靠某个单独功能“消灭”幽灵依赖，而是通过<strong>更严格的目录结构和依赖暴露规则</strong>，大幅减少了这类问题。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="九拿一个实际场景来理解">九、拿一个实际场景来理解<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E4%B9%9D%E6%8B%BF%E4%B8%80%E4%B8%AA%E5%AE%9E%E9%99%85%E5%9C%BA%E6%99%AF%E6%9D%A5%E7%90%86%E8%A7%A3" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>假设现在有两个项目都安装了 <code>lodash@4.17.21</code>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="传统思路">传统思路<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E4%BC%A0%E7%BB%9F%E6%80%9D%E8%B7%AF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>两个项目各自的 <code>node_modules</code> 都放一份 <code>lodash</code>。</p>
<p>结果就是：</p>
<ul>
<li>磁盘里有两份几乎一样的文件</li>
<li>每个项目都要各自维护一份</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="pnpm-思路">pnpm 思路<a href="https://blog.kuizuo.me/blog/how-pnpm-works#pnpm-%E6%80%9D%E8%B7%AF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>pnpm 会更像这样：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">全局 store</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── lodash 的真实文件（只存一份）</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">project-a/node_modules/.pnpm/.../lodash</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── 通过硬链接复用 store 内容</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">project-b/node_modules/.pnpm/.../lodash</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── 通过硬链接复用 store 内容</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">project-a/node_modules/lodash</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── 软链接到 .pnpm 里的 lodash</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">project-b/node_modules/lodash</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">└── 软链接到 .pnpm 里的 lodash</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>这样一来：</p>
<ul>
<li>store 里只有一份真实内容</li>
<li>两个项目都能正常使用</li>
<li>Node.js 仍然能从熟悉的位置找到包</li>
</ul>
<p>这就是 pnpm 这套设计最巧妙的地方。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十那-pnpm-为什么通常也更快">十、那 pnpm 为什么通常也更快？<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E5%8D%81%E9%82%A3-pnpm-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%9A%E5%B8%B8%E4%B9%9F%E6%9B%B4%E5%BF%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>很多人一提 pnpm，会先想到“省空间”，但它的速度优势也很明显。</p>
<p>原因通常有这些：</p>
<ul>
<li>已下载过的包可以直接复用</li>
<li>已存在的文件可以通过链接快速建立关系</li>
<li>不需要在多个项目里反复写入同样的大量文件</li>
</ul>
<p>尤其在这些场景下，pnpm 的优势会更明显：</p>
<ul>
<li>你本地有很多项目</li>
<li>频繁切分支重装依赖</li>
<li>Monorepo 依赖规模大</li>
<li>CI 想尽量减少安装成本</li>
</ul>
<p>所以 pnpm 的快，并不只是“下载快”，更重要的是：</p>
<p><strong>它减少了重复存储和重复写入。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十一底层术语不用全背记住这张脑图就够了">十一、底层术语不用全背，记住这张脑图就够了<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E5%8D%81%E4%B8%80%E5%BA%95%E5%B1%82%E6%9C%AF%E8%AF%AD%E4%B8%8D%E7%94%A8%E5%85%A8%E8%83%8C%E8%AE%B0%E4%BD%8F%E8%BF%99%E5%BC%A0%E8%84%91%E5%9B%BE%E5%B0%B1%E5%A4%9F%E4%BA%86" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果你已经看到有点晕了，可以直接记这个简化版：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-内容寻址存储">1. 内容寻址存储<a href="https://blog.kuizuo.me/blog/how-pnpm-works#1-%E5%86%85%E5%AE%B9%E5%AF%BB%E5%9D%80%E5%AD%98%E5%82%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>作用：<strong>让相同内容只存一份。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-硬链接">2. 硬链接<a href="https://blog.kuizuo.me/blog/how-pnpm-works#2-%E7%A1%AC%E9%93%BE%E6%8E%A5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>作用：<strong>让多个项目看起来都有这个文件，但底层共享同一份数据。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-软链接">3. 软链接<a href="https://blog.kuizuo.me/blog/how-pnpm-works#3-%E8%BD%AF%E9%93%BE%E6%8E%A5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>作用：<strong>让 Node.js 还能按熟悉的 <code>node_modules</code> 路径找到包。</strong></p>
<p>把这三个点串起来，就是：</p>
<div class="language-text codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-text codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">全局 store 存真实内容</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">        ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">.pnpm 通过硬链接复用内容</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">        ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">node_modules 通过软链接把包暴露出来</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>如果你能记住这张图，pnpm 的底层思路基本就不会再混了。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十二最后用一句人话总结-pnpm">十二、最后用一句人话总结 pnpm<a href="https://blog.kuizuo.me/blog/how-pnpm-works#%E5%8D%81%E4%BA%8C%E6%9C%80%E5%90%8E%E7%94%A8%E4%B8%80%E5%8F%A5%E4%BA%BA%E8%AF%9D%E6%80%BB%E7%BB%93-pnpm" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果要把 pnpm 的原理压缩成一句最容易记住的话，那就是：</p>
<p><strong>pnpm 先把依赖统一存到全局，再通过硬链接复用文件，通过软链接兼容 Node.js 的查找方式。</strong></p>
<p>所以它最终实现了三件事：</p>
<ul>
<li>相同依赖尽量只存一份</li>
<li>项目里依然可以像平常一样 <code>import</code> / <code>require</code></li>
<li>依赖关系更严格，更不容易藏问题</li>
</ul>
<p>这也是为什么越来越多中大型项目、Monorepo 项目会选择 pnpm。</p>
<p>它不只是“另一个 install 工具”，而是一套更偏工程化的依赖管理方案。</p>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="pnpm" term="pnpm"/>
        <category label="Node.js" term="Node.js"/>
        <category label="包管理" term="包管理"/>
        <category label="前端工程化" term="前端工程化"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[从输入到成交：我如何把 RFQ 从站点功能升级成 Global 级交易能力平台]]></title>
        <id>https://blog.kuizuo.me/blog/rfq-platform-upgrade</id>
        <link href="https://blog.kuizuo.me/blog/rfq-platform-upgrade"/>
        <updated>2026-03-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[这不是一篇组件实现总结，而是一篇更适合面试、晋升答辩和项目复盘的业务型文章。我想讲清楚：RFQ 为什么重要、它原来卡在哪里、我做了哪些关键改造，以及它如何从站点功能升级为 Global 级交易能力平台。]]></summary>
        <content type="html"><![CDATA[<p>RFQ（Request For Quote）是一种面向机构和大额交易用户的询价交易模式。它通过聚合多做市商报价，为用户提供更确定的成交价格和更优的流动性。</p>
<p>如果从业务链路去看，一个完整的 RFQ 系统通常会串起这些能力：</p>
<ul>
<li>用户准入与 KYC 校验</li>
<li>询价请求与报价返回</li>
<li>做市商报价聚合</li>
<li>用户确认与订单执行</li>
<li>基于 OTC 的清算结算</li>
<li>与 FX、用户风控、定投等业务联动</li>
</ul>
<p>所以 RFQ 从来都不只是“一个交易页面”，而是一套高度业务化的交易系统能力。</p>
<p><img decoding="async" loading="lazy" src="https://cdn.nlark.com/yuque/0/2026/png/1554318/1774247528032-e9672edb-ee9a-4044-9510-f119d3278730.png" alt="" class="img_OpE3"></p>
<p><img decoding="async" loading="lazy" src="https://cdn.nlark.com/yuque/0/2026/png/1554318/1774247740140-9e6bb1aa-ac8e-4b30-941d-e50352d32d70.png" alt="" class="img_OpE3"></p>
<p>这篇文章不是写“某个组件怎么写”，而是更偏<strong>项目复盘、面试表达、晋升答辩</strong>的文章。</p>
<p>我想讲清楚的是：</p>
<ul>
<li>RFQ 为什么值得单独做成公司级能力</li>
<li>它早期为什么难做、卡在哪里</li>
<li>我做了哪些关键改造</li>
<li>这套能力给业务和团队带来了什么价值</li>
<li>它未来还能往哪个方向继续演进</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一为什么-rfq-值得单独做成公司级能力">一、为什么 RFQ 值得单独做成公司级能力<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E4%B8%80%E4%B8%BA%E4%BB%80%E4%B9%88-rfq-%E5%80%BC%E5%BE%97%E5%8D%95%E7%8B%AC%E5%81%9A%E6%88%90%E5%85%AC%E5%8F%B8%E7%BA%A7%E8%83%BD%E5%8A%9B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>很多人看到 RFQ，第一反应可能是：</p>
<p><strong>不就是一个“获取报价 -&gt; 确认成交”的交易页面吗？</strong></p>
<p>但如果真正拆开看，你会发现它远远不只是一个前端页面。</p>
<p>用户表面上看到的流程只有几步：</p>
<ol>
<li>选择交易对</li>
<li>输入金额或数量</li>
<li>获取报价</li>
<li>确认成交</li>
<li>查看订单结果</li>
</ol>
<p>但在系统内部，这背后同时串起了：</p>
<ul>
<li>用户身份与 KYC 校验</li>
<li>交易权限与合规控制</li>
<li>实时报价与流动性路由</li>
<li>订单创建与状态流转</li>
<li>资产冻结与资产更新</li>
<li>多站点、多币种、多法币规则适配</li>
<li>活动、定投、FX 等业务能力复用</li>
</ul>
<p>所以 RFQ 不是“一个交易页面”，而是<strong>交易能力、风控能力、资产能力、合规能力、站点能力的交汇点</strong>。</p>
<p>一旦这条链路足够稳定、足够抽象，它就不再只服务某一个页面，而会变成 Global、HK、ID 多市场共享的交易底座。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二项目背景rfq-早期到底卡在哪里">二、项目背景：RFQ 早期到底卡在哪里<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E4%BA%8C%E9%A1%B9%E7%9B%AE%E8%83%8C%E6%99%AFrfq-%E6%97%A9%E6%9C%9F%E5%88%B0%E5%BA%95%E5%8D%A1%E5%9C%A8%E5%93%AA%E9%87%8C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>RFQ 虽然是公司 Global 体系中的核心交易能力，但在早期，它并不是一个真正的平台化系统，而更像是一组“为单一站点快速拼出来的定制功能”。</p>
<p>当时主要有 4 类问题。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-站点定制化严重复用能力弱">1. 站点定制化严重，复用能力弱<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#1-%E7%AB%99%E7%82%B9%E5%AE%9A%E5%88%B6%E5%8C%96%E4%B8%A5%E9%87%8D%E5%A4%8D%E7%94%A8%E8%83%BD%E5%8A%9B%E5%BC%B1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>香港站、全球站、印尼站在业务规则、交易对、法币、合规要求上都有差异</li>
<li>早期实现更偏站点单独适配</li>
<li>每次开新站、上新玩法，都需要重新拼流程、重新接接口、重新做风控兜底</li>
</ul>
<p>结果就是：</p>
<ul>
<li>新站点上线周期长</li>
<li>维护成本高</li>
<li>逻辑容易分叉</li>
<li>一旦核心链路有调整，需要多处同步修改</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-交易链路长用户决策成本高">2. 交易链路长，用户决策成本高<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#2-%E4%BA%A4%E6%98%93%E9%93%BE%E8%B7%AF%E9%95%BF%E7%94%A8%E6%88%B7%E5%86%B3%E7%AD%96%E6%88%90%E6%9C%AC%E9%AB%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>RFQ 是即时报价交易，用户对“快”和“确定性”的敏感度非常高。</p>
<p>如果流程冗长、提示割裂、状态不清晰，就很容易出现这些问题：</p>
<ul>
<li>用户不知道当前价格是不是可执行价格</li>
<li>用户点了按钮后，不知道系统现在处于哪一步</li>
<li>价格过期、余额不足、KYC 限制等错误，在错误的时机才暴露</li>
</ul>
<p>这种问题本质上会直接伤害成交转化率。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-业务扩展受限难以支撑-fx定投等新形态">3. 业务扩展受限，难以支撑 FX、定投等新形态<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#3-%E4%B8%9A%E5%8A%A1%E6%89%A9%E5%B1%95%E5%8F%97%E9%99%90%E9%9A%BE%E4%BB%A5%E6%94%AF%E6%92%91-fx%E5%AE%9A%E6%8A%95%E7%AD%89%E6%96%B0%E5%BD%A2%E6%80%81" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>RFQ 不只是简单的买卖币能力，随着业务发展，它开始承接：</p>
<ul>
<li>FX 外汇交易</li>
<li>定投交易</li>
<li>Stable 类营销与收益活动</li>
<li>多站点扩张</li>
</ul>
<p>如果底层交易模型不统一，后续每增加一个业务形态，都会变成一次新的“局部开发”，系统很快就会进入不可控状态。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-缺少统一的数据指标与效果评估体系">4. 缺少统一的数据指标与效果评估体系<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#4-%E7%BC%BA%E5%B0%91%E7%BB%9F%E4%B8%80%E7%9A%84%E6%95%B0%E6%8D%AE%E6%8C%87%E6%A0%87%E4%B8%8E%E6%95%88%E6%9E%9C%E8%AF%84%E4%BC%B0%E4%BD%93%E7%B3%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>团队当时很难回答这些关键问题：</p>
<ul>
<li>询价成功率到底是多少</li>
<li>哪一步流失最严重</li>
<li>下单失败主要集中在哪些错误码</li>
<li>某次流程优化到底带来了多少业务改善</li>
</ul>
<p>没有数据闭环，就很难从“主观感觉优化”走向“数据驱动优化”。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三项目目标不是做个页面而是搭一套可复制的交易底座">三、项目目标：不是做个页面，而是搭一套可复制的交易底座<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E4%B8%89%E9%A1%B9%E7%9B%AE%E7%9B%AE%E6%A0%87%E4%B8%8D%E6%98%AF%E5%81%9A%E4%B8%AA%E9%A1%B5%E9%9D%A2%E8%80%8C%E6%98%AF%E6%90%AD%E4%B8%80%E5%A5%97%E5%8F%AF%E5%A4%8D%E5%88%B6%E7%9A%84%E4%BA%A4%E6%98%93%E5%BA%95%E5%BA%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这个项目的目标，不是简单优化某一版 RFQ 页面，而是围绕 RFQ 全链路完成一次平台化升级。</p>
<p>我把它拆成 5 个明确目标：</p>
<ol>
<li>打造可复用、可扩展的 RFQ 交易能力</li>
<li>优化交易体验，提高询价成功率与成交转化率</li>
<li>支撑 FX、定投等多业务形态发展</li>
<li>实现多站点快速复制，降低交付成本</li>
<li>建立数据驱动体系，支撑持续优化</li>
</ol>
<p>如果用一句更适合面试表达的话来概括，这个项目做的是：</p>
<p><strong>把一个站点级交易功能，升级成公司级可复用的交易能力平台。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四项目产出的直接价值">四、项目产出的直接价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E5%9B%9B%E9%A1%B9%E7%9B%AE%E4%BA%A7%E5%87%BA%E7%9A%84%E7%9B%B4%E6%8E%A5%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>围绕 RFQ 交易能力，团队最终完成了：</p>
<ul>
<li>覆盖香港站、全球站、印尼站的多站点建设</li>
<li>落地 RFQ、FX、定投等多种交易形态</li>
<li>打通从交易入口、实时报价、确认成交、订单管理到资产与合规提示的完整链路</li>
<li>将复杂的合规与交易能力规模化交付到不同市场</li>
</ul>
<p>这些成果直接支撑了公司多区域业务开展与实际收入增长。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-打造-global-级核心交易能力">1. 打造 Global 级核心交易能力<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#1-%E6%89%93%E9%80%A0-global-%E7%BA%A7%E6%A0%B8%E5%BF%83%E4%BA%A4%E6%98%93%E8%83%BD%E5%8A%9B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>RFQ 作为 Global 站最重要的交易能力之一，从站点定制实现升级为可复用、可扩展的交易能力平台</li>
<li>RFQ 能力快速复用到印尼站并稳定运行，显著降低新站点交易能力建设成本</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-交易体验与业务指标显著提升">2. 交易体验与业务指标显著提升<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#2-%E4%BA%A4%E6%98%93%E4%BD%93%E9%AA%8C%E4%B8%8E%E4%B8%9A%E5%8A%A1%E6%8C%87%E6%A0%87%E6%98%BE%E8%91%97%E6%8F%90%E5%8D%87" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>通过“询价前置 + 流程简化 + 交易链路优化”，RFQ 交易从“可用”升级为“高效、稳定、可规模化放量”的核心能力。</p>
<p>这类优化直接带来三类收益：</p>
<ul>
<li>降低用户交易决策成本</li>
<li>提升询价成功率与下单效率</li>
<li>降低交易中断与无效操作比例</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-数据驱动优化形成可量化增长闭环">3. 数据驱动优化，形成可量化增长闭环<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#3-%E6%95%B0%E6%8D%AE%E9%A9%B1%E5%8A%A8%E4%BC%98%E5%8C%96%E5%BD%A2%E6%88%90%E5%8F%AF%E9%87%8F%E5%8C%96%E5%A2%9E%E9%95%BF%E9%97%AD%E7%8E%AF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>建立 RFQ 关键指标统计与分析体系</li>
<li>可以持续跟踪询价、确认、下单、执行等关键节点</li>
<li>为后续交易能力持续优化提供数据决策基础</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五rfq-全链路拆解从交易入口到成交回执">五、RFQ 全链路拆解：从交易入口到成交回执<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E4%BA%94rfq-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%8B%86%E8%A7%A3%E4%BB%8E%E4%BA%A4%E6%98%93%E5%85%A5%E5%8F%A3%E5%88%B0%E6%88%90%E4%BA%A4%E5%9B%9E%E6%89%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果从用户视角看，RFQ 的完整链路大致可以拆成 7 步。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-交易入口">1. 交易入口<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#1-%E4%BA%A4%E6%98%93%E5%85%A5%E5%8F%A3" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>交易入口并不只来自一个页面，它通常分布在多个流量来源中：</p>
<ul>
<li>首页 Banner 导流</li>
<li>行情页 / Price 页跳转</li>
<li>历史交易页返回</li>
<li>活动页或定投关联入口</li>
</ul>
<p>在当前项目里，对外很多入口仍然叫 <code>rfq</code>，但实际承载已经统一收敛到 <code>flash</code> 路由。</p>
<p>这一步的意义不只是路由重定向，而是：</p>
<p><strong>前台入口统一、后台能力统一。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-初始化交易上下文">2. 初始化交易上下文<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#2-%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%A4%E6%98%93%E4%B8%8A%E4%B8%8B%E6%96%87" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>用户进入页面后，系统需要先完成一轮交易上下文准备：</p>
<ul>
<li>解析 URL 中的默认币对</li>
<li>获取支持交易的币种、法币、交易对关系</li>
<li>获取交易精度、最小值、最大值、交易状态</li>
<li>读取用户最近一次成功交易的币对偏好</li>
<li>初始化当前 tab：<code>BUY / SELL / CONVERT</code></li>
</ul>
<p>这一层决定了页面是不是“可交易”，也决定了后续参数映射是不是正确。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-实时预询价">3. 实时预询价<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#3-%E5%AE%9E%E6%97%B6%E9%A2%84%E8%AF%A2%E4%BB%B7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>当用户输入金额或数量后，系统不会等到最后一步才报价，而是先做一轮实时预询价。</p>
<p>这一步的业务意义很大：</p>
<ul>
<li>让用户先看到真实可参考的价格</li>
<li>在输入阶段尽早暴露不合法金额、精度或限额问题</li>
<li>提前缩短用户的决策路径</li>
</ul>
<p>从体验上看，这一步只是页面上实时展示汇率与倒计时。<br>
<!-- -->但从业务上看，这一步相当于把“价格确认”前置了。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-正式询价">4. 正式询价<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#4-%E6%AD%A3%E5%BC%8F%E8%AF%A2%E4%BB%B7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>用户点击 <code>Get Quote</code> 后，系统发起正式询价，拿到：</p>
<ul>
<li><code>quoteId</code></li>
<li><code>quotePrice</code></li>
<li><code>quoteQuantity</code></li>
<li><code>quoteTotalAmount</code></li>
<li><code>ttl</code> 或等效报价有效时间</li>
</ul>
<p>这一阶段的关键不是“再调一次接口”，而是：</p>
<ul>
<li>把展示级报价切换成可执行报价</li>
<li>将这次报价纳入一个可下单、可追踪、可过期控制的正式交易上下文</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-报价确认">5. 报价确认<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#5-%E6%8A%A5%E4%BB%B7%E7%A1%AE%E8%AE%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>用户进入确认页后，页面会展示：</p>
<ul>
<li>买卖双方币种</li>
<li>支付金额 / 接收金额</li>
<li>执行价格</li>
<li>报价倒计时</li>
<li>零手续费或活动权益提示</li>
</ul>
<p>这一步是整个 RFQ 的核心信任节点。</p>
<p>因为用户不是“盲下单”，而是在明确确认当前成交条件后才点击确认。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="6-下单与执行">6. 下单与执行<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#6-%E4%B8%8B%E5%8D%95%E4%B8%8E%E6%89%A7%E8%A1%8C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>当用户点击 <code>Confirm</code> 后，系统进入真正的交易执行阶段：</p>
<ol>
<li>使用 <code>quoteId</code> 发起下单</li>
<li>服务端校验 quote 是否仍有效</li>
<li>校验用户资格、风控规则、订单幂等</li>
<li>冻结对应资产</li>
<li>异步向流动性侧或 MM 发起对冲 / 执行</li>
<li>更新订单状态</li>
<li>前端轮询订单执行结果</li>
</ol>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="7-结果回传与后续动作">7. 结果回传与后续动作<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#7-%E7%BB%93%E6%9E%9C%E5%9B%9E%E4%BC%A0%E4%B8%8E%E5%90%8E%E7%BB%AD%E5%8A%A8%E4%BD%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>如果成功：</p>
<ul>
<li>展示成交结果页</li>
<li>更新用户资产</li>
<li>引导到定投、划转、后续交易等页面</li>
</ul>
<p>如果失败：</p>
<ul>
<li>展示失败原因</li>
<li>允许用户回到交易页重新发起流程</li>
</ul>
<p>这意味着 RFQ 不只是“下单”，它还承担了<strong>成交回执、资产反馈、后续业务转化</strong>三个职责。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六后端核心时序quote-是怎么生成的order-是怎么执行的">六、后端核心时序：quote 是怎么生成的，order 是怎么执行的<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E5%85%AD%E5%90%8E%E7%AB%AF%E6%A0%B8%E5%BF%83%E6%97%B6%E5%BA%8Fquote-%E6%98%AF%E6%80%8E%E4%B9%88%E7%94%9F%E6%88%90%E7%9A%84order-%E6%98%AF%E6%80%8E%E4%B9%88%E6%89%A7%E8%A1%8C%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>前端看到的是一个交易页面，后端实际跑的是一套多系统协作链路。</p>
<p>结合后端时序，可以把 RFQ 拆成“报价链路”和“执行链路”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-报价链路quote-是怎么出来的">1. 报价链路：quote 是怎么出来的<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#1-%E6%8A%A5%E4%BB%B7%E9%93%BE%E8%B7%AFquote-%E6%98%AF%E6%80%8E%E4%B9%88%E5%87%BA%E6%9D%A5%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>报价不是简单读一个行情价格返回给前端，而是一个带合规、汇率、MM 价格、缓存的组合过程。</p>
<p>大致过程如下：</p>
<ol>
<li>用户发起某个币种与法币的询价，例如数币 / IDR</li>
<li>RFQ 服务生成 <code>quoteId</code></li>
<li>查询用户信息，获取 KYC 状态、交易权限、用户等级</li>
<li>查询当前交易对的行情价格</li>
<li>校验用户权限与币对交易状态</li>
<li>根据输入类型计算币数量、法币金额和对应价格</li>
<li>查询法币与 USD 的汇率，例如 <code>IDR/USD</code></li>
<li>基于数币 / USD 与法币 / USD 汇率，计算对应法币报价</li>
<li>向 MM 侧获取或校准初始报价</li>
<li>计算最终对用户展示的成交价格</li>
<li>把 <code>quoteInfo</code> 存入 Redis</li>
<li>把法币 / USD 汇率等关键上下文与 <code>quoteId</code> 做缓存关联</li>
<li>将报价返回给前端</li>
</ol>
<p>这条链路说明了一个很重要的点：</p>
<p><strong>RFQ 的 quote 不是一个“单纯的价格结果”，而是一个“带业务上下文、带时效、可下单、可追踪”的交易快照。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-执行链路order-是怎么完成的">2. 执行链路：order 是怎么完成的<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#2-%E6%89%A7%E8%A1%8C%E9%93%BE%E8%B7%AForder-%E6%98%AF%E6%80%8E%E4%B9%88%E5%AE%8C%E6%88%90%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>当用户点击确认后，系统不是直接成交，而是进入订单执行状态机。</p>
<p>核心步骤如下：</p>
<ol>
<li>从 Redis 取回 <code>QuoteDetail</code></li>
<li>从 Redis 取回与该 quote 绑定的汇率上下文，例如 <code>IDR/USD</code></li>
<li>做幂等与有效性检查</li>
<li>创建订单并记录关键汇率字段</li>
<li>写入订单状态流转记录</li>
<li>做分布式锁控制，避免重复执行</li>
<li>冻结用户资产</li>
<li>异步向 spot / MM 发起执行或对冲</li>
<li>轮询 MM 结果</li>
<li>如果成功，更新订单状态并继续写入状态流转记录</li>
<li>前端轮询订单结果并展示成功或失败页</li>
</ol>
<p>这条链路的核心价值在于：</p>
<ul>
<li>保证订单执行具备时效一致性</li>
<li>保证报价与下单使用的是同一份业务快照</li>
<li>保证资产冻结、状态流转、外部执行三者可追踪</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七为什么这个项目难做业务难点与技术难点">七、为什么这个项目难做：业务难点与技术难点<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E4%B8%83%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%99%E4%B8%AA%E9%A1%B9%E7%9B%AE%E9%9A%BE%E5%81%9A%E4%B8%9A%E5%8A%A1%E9%9A%BE%E7%82%B9%E4%B8%8E%E6%8A%80%E6%9C%AF%E9%9A%BE%E7%82%B9" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这类项目真正的难，不在“页面多复杂”，而在：</p>
<p><strong>每一步都牵涉多个业务系统，且任何一步出错都会直接影响交易结果。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-参数语义复杂尤其是-convert">1. 参数语义复杂，尤其是 Convert<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#1-%E5%8F%82%E6%95%B0%E8%AF%AD%E4%B9%89%E5%A4%8D%E6%9D%82%E5%B0%A4%E5%85%B6%E6%98%AF-convert" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>Buy、Sell 看起来相对清晰，但 Convert 会引入一个非常典型的问题：</p>
<ul>
<li>用户理解的是 <code>from -&gt; to</code></li>
<li>系统执行的是 <code>tradeCurrency + settlementCurrency + side</code></li>
</ul>
<p>也就是说，前端看到的输入方向，并不等于后端实际执行方向。</p>
<p>当交易对只存在单向时，系统还需要动态判断应该走 Buy 还是 Sell 语义。</p>
<p>这件事难的地方不是写 if/else，而是：</p>
<ul>
<li>任何一处映射不一致，都会造成“报价展示正确、下单参数错误”</li>
<li>同一套页面逻辑要兼容 Buy / Sell / Convert 多种模式</li>
<li>还要适配 FX、定投等业务继续复用</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-时效性与一致性要求都很高">2. 时效性与一致性要求都很高<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#2-%E6%97%B6%E6%95%88%E6%80%A7%E4%B8%8E%E4%B8%80%E8%87%B4%E6%80%A7%E8%A6%81%E6%B1%82%E9%83%BD%E5%BE%88%E9%AB%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>RFQ 不是长期有效价格，quote 天生带 TTL。</p>
<p>一旦用户停留时间较长，就会出现“用户看到的价格”和“系统可执行价格”之间的时间差。</p>
<p>所以系统必须处理好：</p>
<ul>
<li>预询价与正式询价的衔接</li>
<li>确认页的报价刷新</li>
<li>下单前 quote 过期检查</li>
<li>重新询价后的状态一致性</li>
</ul>
<p>这类问题如果处理不好，最常见的后果就是：</p>
<ul>
<li>用户频繁下单失败</li>
<li>用户感知价格跳动大</li>
<li>投诉价格不一致</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-风控门禁不是一个点而是一条链">3. 风控门禁不是一个点，而是一条链<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#3-%E9%A3%8E%E6%8E%A7%E9%97%A8%E7%A6%81%E4%B8%8D%E6%98%AF%E4%B8%80%E4%B8%AA%E7%82%B9%E8%80%8C%E6%98%AF%E4%B8%80%E6%9D%A1%E9%93%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>RFQ 会同时受这些因素约束：</p>
<ul>
<li>是否登录</li>
<li>是否完成 KYC</li>
<li>BOA / POA 等合规审核状态</li>
<li>是否具备该币对交易权限</li>
<li>最小 / 最大订单限制</li>
<li>精度是否合法</li>
<li>余额是否足够</li>
<li>币对交易状态是否开放</li>
</ul>
<p>这意味着风控不是某个弹窗，而是一套贯穿“输入 -&gt; 报价 -&gt; 下单 -&gt; 执行”的门禁系统。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-多站点多法币多业务复用">4. 多站点、多法币、多业务复用<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#4-%E5%A4%9A%E7%AB%99%E7%82%B9%E5%A4%9A%E6%B3%95%E5%B8%81%E5%A4%9A%E4%B8%9A%E5%8A%A1%E5%A4%8D%E7%94%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>站点一多，复杂度会指数级上升。</p>
<p>因为不同站点会有不同：</p>
<ul>
<li>支持法币</li>
<li>合规规则</li>
<li>活动策略</li>
<li>页面入口</li>
<li>本地化文案</li>
<li>交易玩法</li>
</ul>
<p>如果系统是站点定制模式，那每开一个市场都像重做一次 RFQ。<br>
<!-- -->而如果想做成平台能力，就必须把“站点差异”沉淀成“规则与配置”，而不是“复制代码”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-前端状态天然容易失控">5. 前端状态天然容易失控<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#5-%E5%89%8D%E7%AB%AF%E7%8A%B6%E6%80%81%E5%A4%A9%E7%84%B6%E5%AE%B9%E6%98%93%E5%A4%B1%E6%8E%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>前端这条链路里同时存在：</p>
<ul>
<li>tab 状态</li>
<li>币对状态</li>
<li>输入状态</li>
<li>fast quote 状态</li>
<li>quote confirm 状态</li>
<li>order result 状态</li>
<li>资产信息</li>
<li>用户信息</li>
<li>本地缓存</li>
</ul>
<p>再叠加防抖、轮询、异步接口、切换交易对，就很容易出现：</p>
<ul>
<li>状态覆盖</li>
<li>旧请求污染新结果</li>
<li>边界条件难兜底</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八为什么这个项目有价值业务痛点与改造收益">八、为什么这个项目有价值：业务痛点与改造收益<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E5%85%AB%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%99%E4%B8%AA%E9%A1%B9%E7%9B%AE%E6%9C%89%E4%BB%B7%E5%80%BC%E4%B8%9A%E5%8A%A1%E7%97%9B%E7%82%B9%E4%B8%8E%E6%94%B9%E9%80%A0%E6%94%B6%E7%9B%8A" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>项目真正有说服力的地方，不是“我重构了代码”，而是：</p>
<p><strong>我解决了什么业务痛点，并让业务更容易增长。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-早期-rfq-只能算能用不能算可放量">1. 早期 RFQ 只能算能用，不能算可放量<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#1-%E6%97%A9%E6%9C%9F-rfq-%E5%8F%AA%E8%83%BD%E7%AE%97%E8%83%BD%E7%94%A8%E4%B8%8D%E8%83%BD%E7%AE%97%E5%8F%AF%E6%94%BE%E9%87%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>早期 RFQ 的问题不是不能交易，而是：</p>
<ul>
<li>流程割裂</li>
<li>体验不稳定</li>
<li>复用成本高</li>
<li>交易链路容易中断</li>
</ul>
<p>这类系统在业务初期还能顶住，但一旦要做 Global 主交易能力，就会暴露出明显瓶颈。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-每个新业务都在重复造轮子">2. 每个新业务都在“重复造轮子”<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#2-%E6%AF%8F%E4%B8%AA%E6%96%B0%E4%B8%9A%E5%8A%A1%E9%83%BD%E5%9C%A8%E9%87%8D%E5%A4%8D%E9%80%A0%E8%BD%AE%E5%AD%90" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>如果 RFQ 只是一个单点功能，那么：</p>
<ul>
<li>做 FX 需要再适配一套</li>
<li>做定投需要再接一套</li>
<li>做新站点还要再适配一轮</li>
</ul>
<p>长期来看，这种模式的成本不只是开发成本，更是<strong>组织协作成本和持续维护成本</strong>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-缺少数据闭环优化难以自证">3. 缺少数据闭环，优化难以自证<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#3-%E7%BC%BA%E5%B0%91%E6%95%B0%E6%8D%AE%E9%97%AD%E7%8E%AF%E4%BC%98%E5%8C%96%E9%9A%BE%E4%BB%A5%E8%87%AA%E8%AF%81" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>很多交易系统都会做优化，但如果没有指标体系，就只能停留在“感觉变好了”。</p>
<p>而交易业务最怕的就是：</p>
<ul>
<li>不知道哪一步流失最严重</li>
<li>不知道失败码主要集中在哪</li>
<li>不知道流程优化有没有真正提高成交效率</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-这次改造带来的核心收益">4. 这次改造带来的核心收益<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#4-%E8%BF%99%E6%AC%A1%E6%94%B9%E9%80%A0%E5%B8%A6%E6%9D%A5%E7%9A%84%E6%A0%B8%E5%BF%83%E6%94%B6%E7%9B%8A" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这次 RFQ 项目之后，团队获得的不是“一个新版页面”，而是三层收益。</p>
<p>第一层是业务收益：</p>
<ul>
<li>RFQ 交易量显著提升</li>
<li>询价成功率与成交转化率明显提升</li>
<li>RFQ 成为支撑 Global、ID、HK 的重要交易基础能力</li>
</ul>
<p>第二层是组织效率收益：</p>
<ul>
<li>新站点 RFQ 能力建设周期大幅缩短</li>
<li>复用 Global 成熟能力，减少重复开发与维护成本</li>
<li>交易链路稳定后，运营与问题处理成本显著下降</li>
</ul>
<p>第三层是技术资产收益：</p>
<ul>
<li>一套核心系统支撑多区域、多业务</li>
<li>统一交易模型与技术标准</li>
<li>为 FX、定投以及后续新型交易产品提供基础能力</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="九项目执行过程与阶段性落地">九、项目执行过程与阶段性落地<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E4%B9%9D%E9%A1%B9%E7%9B%AE%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E4%B8%8E%E9%98%B6%E6%AE%B5%E6%80%A7%E8%90%BD%E5%9C%B0" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这个项目不是一次性重写完成的，而是随着业务节奏逐步推进的。</p>
<table><thead><tr><th>时间</th><th>项目举措</th><th>详细描述</th></tr></thead><tbody><tr><td>2025.07.22</td><td>HK RFQ 全新改版</td><td>询价前置，在下单前完成报价判断，减少无效操作</td></tr><tr><td>2025.08.13</td><td>Global RFQ 定投上线</td><td>拓展定投交易形态</td></tr><tr><td>2025.09.08</td><td>HK FX 上线</td><td>支持外汇交易场景</td></tr><tr><td>2025.09.24</td><td>HK RFQ 定投上线</td><td>定投能力复用，多业务能力落地</td></tr><tr><td>2025.10.28</td><td>Global RFQ 流程优化</td><td>简化交易步骤，减少用户等待时间与中断点，提升整体交易流畅度</td></tr><tr><td>2025.10.28</td><td>Global RFQ 改版升级</td><td>对 Global 站 RFQ 核心链路进行系统性重构，提升可维护性与扩展性</td></tr><tr><td>2025.11.26</td><td>ID RFQ 快速复用</td><td>快速支持印尼站上线</td></tr><tr><td>2025.12.03</td><td>HK RFQ 迁移与优化</td><td>完成 RFQ 能力迁移并统一交易模型，支持 FX 与定投等多业务场景</td></tr><tr><td>2025.12.15</td><td>数据统计与效果评估</td><td>建立 RFQ 关键指标体系，持续评估优化效果</td></tr></tbody></table>
<p>如果把这些动作抽象一下，会发现项目推进路径非常清晰：</p>
<ol>
<li>先优化核心链路体验</li>
<li>再扩展多业务玩法</li>
<li>再做 Global 化与平台化升级</li>
<li>最后补齐数据闭环与效果评估</li>
</ol>
<p>这也是一个很适合在面试里讲的“从战术优化走向战略能力建设”的项目路径。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十项目结果业务价值效率价值技术价值">十、项目结果：业务价值、效率价值、技术价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E5%8D%81%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%9C%E4%B8%9A%E5%8A%A1%E4%BB%B7%E5%80%BC%E6%95%88%E7%8E%87%E4%BB%B7%E5%80%BC%E6%8A%80%E6%9C%AF%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-业务价值">1. 业务价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#1-%E4%B8%9A%E5%8A%A1%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>RFQ 交易量显著提升，成为 Global 主交易能力中的持续放量模块</li>
<li>询价成功率与成交转化率明显提升</li>
<li>RFQ 成为支撑 Global、ID、HK 的核心交易基础能力</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-效率价值">2. 效率价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#2-%E6%95%88%E7%8E%87%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>新站点 RFQ 能力建设周期大幅缩短</li>
<li>复用 Global 成熟能力，减少重复开发与维护成本</li>
<li>链路稳定后，运营与问题处理成本显著下降</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-技术价值">3. 技术价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#3-%E6%8A%80%E6%9C%AF%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>高稳定性：</p>
<ul>
<li>多站点高并发场景下稳定运行</li>
<li>核心链路可靠性持续提升</li>
</ul>
<p>平台化与复用能力：</p>
<ul>
<li>一套核心系统支撑多区域、多业务</li>
<li>统一交易模型与技术标准</li>
</ul>
<p>可持续演进：</p>
<ul>
<li>支持 FX、定投等持续扩展</li>
<li>为后续新型交易产品提供基础能力</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十一这段经历在面试里应该怎么讲">十一、这段经历在面试里应该怎么讲<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E5%8D%81%E4%B8%80%E8%BF%99%E6%AE%B5%E7%BB%8F%E5%8E%86%E5%9C%A8%E9%9D%A2%E8%AF%95%E9%87%8C%E5%BA%94%E8%AF%A5%E6%80%8E%E4%B9%88%E8%AE%B2" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果要把这个项目讲得“既懂业务又懂系统”，建议不要只讲“我做了一个 RFQ 页面”，而要按下面这个结构来讲。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-先讲项目级别">1. 先讲项目级别<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#1-%E5%85%88%E8%AE%B2%E9%A1%B9%E7%9B%AE%E7%BA%A7%E5%88%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>可以先定调：</p>
<p><strong>我做的不是单一页面优化，而是围绕 RFQ 核心交易链路做了一次平台化升级，目标是让这套能力可以支撑 Global、HK、ID 多站点，以及 FX、定投等多业务形态。</strong></p>
<p>这句话能直接把项目从“需求开发”拉升到“核心能力建设”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-再讲你解决的核心矛盾">2. 再讲你解决的核心矛盾<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#2-%E5%86%8D%E8%AE%B2%E4%BD%A0%E8%A7%A3%E5%86%B3%E7%9A%84%E6%A0%B8%E5%BF%83%E7%9F%9B%E7%9B%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>建议用“旧系统为什么不行”来讲，而不是直接堆功能点：</p>
<ul>
<li>旧系统站点定制化严重，复用能力差</li>
<li>交易链路长，用户决策成本高</li>
<li>参数语义复杂，尤其是 Convert 映射容易出错</li>
<li>缺少指标体系，优化难以闭环</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-然后讲你的关键动作">3. 然后讲你的关键动作<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#3-%E7%84%B6%E5%90%8E%E8%AE%B2%E4%BD%A0%E7%9A%84%E5%85%B3%E9%94%AE%E5%8A%A8%E4%BD%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>你可以把动作概括成 4 个关键词：</p>
<ul>
<li>流程前置：把询价前置，减少无效操作</li>
<li>模型统一：统一交易模型和参数语义</li>
<li>平台复用：让能力可支撑多站点、多业务</li>
<li>数据闭环：建立关键指标统计与效果评估体系</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-最后讲结果">4. 最后讲结果<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#4-%E6%9C%80%E5%90%8E%E8%AE%B2%E7%BB%93%E6%9E%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>结果不要只讲“上线了”，而要讲三类结果：</p>
<ul>
<li>业务结果：交易量、询价成功率、转化率提升</li>
<li>效率结果：新站点接入更快、复用成本更低</li>
<li>技术结果：系统更稳定、更可扩展、更可持续演进</li>
</ul>
<p>如果面试官继续追问细节，再展开你对下面这些点的理解：</p>
<ul>
<li><code>quote</code> 与 <code>order</code> 的两段式链路</li>
<li>Convert 参数映射</li>
<li>风控门禁前置</li>
<li>多站点差异抽象</li>
<li>数据指标设计</li>
</ul>
<p>基本就能把项目讲深了。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十二项目未来从内部交易系统走向白标-saas-能力">十二、项目未来：从内部交易系统走向白标 SaaS 能力<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E5%8D%81%E4%BA%8C%E9%A1%B9%E7%9B%AE%E6%9C%AA%E6%9D%A5%E4%BB%8E%E5%86%85%E9%83%A8%E4%BA%A4%E6%98%93%E7%B3%BB%E7%BB%9F%E8%B5%B0%E5%90%91%E7%99%BD%E6%A0%87-saas-%E8%83%BD%E5%8A%9B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>我对这个项目未来的理解，不只是继续做几个站点，而是把 RFQ 系统从内部工具升级为可对外输出的白标 SaaS 产品，实现降本增收与生态拓展。</p>
<p>整体可以分三阶段、约 18 个月推进。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第一阶段底座重构">第一阶段：底座重构<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E7%AC%AC%E4%B8%80%E9%98%B6%E6%AE%B5%E5%BA%95%E5%BA%A7%E9%87%8D%E6%9E%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>目标是抽象参数配置、做配置化设计，并拆分报价、风控、结算等核心模块，逐步演进成更轻量、更标准化的服务。</p>
<p>预期收益：</p>
<ul>
<li>减少运营成本</li>
<li>提升复用性</li>
<li>提升稳定性与运维效率</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第二阶段业务扩容">第二阶段：业务扩容<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E7%AC%AC%E4%BA%8C%E9%98%B6%E6%AE%B5%E4%B8%9A%E5%8A%A1%E6%89%A9%E5%AE%B9" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>在现货 RFQ 基础上，逐步纳入更多产品线：</p>
<ul>
<li>合约</li>
<li>OTC</li>
<li>理财</li>
</ul>
<p>最终形成统一报价、统一交易入口、统一撮合与风控逻辑的多产品 RFQ 平台。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第三阶段商业化与生态化">第三阶段：商业化与生态化<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E7%AC%AC%E4%B8%89%E9%98%B6%E6%AE%B5%E5%95%86%E4%B8%9A%E5%8C%96%E4%B8%8E%E7%94%9F%E6%80%81%E5%8C%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>把 RFQ 能力标准化为多租户 SaaS 方案，通过订阅制加“流动性租户”模式，服务线上或线下的中小型 P2P OTC 业务服务商。</p>
<p>这一步一旦跑通，RFQ 的角色就会从：</p>
<ul>
<li>公司内部交易系统</li>
</ul>
<p>升级为：</p>
<ul>
<li>可输出的流动性与交易基础设施产品</li>
</ul>
<p>如果这条路能走通，它的价值就不仅仅是支撑公司现有业务，而是变成新的收入模型与生态能力。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="总结">总结<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>RFQ 项目最值得讲的地方，不是页面有多复杂，而是它把交易、风控、资产、合规、站点扩张、业务复用、数据驱动这些原本分散的能力，整合成了一套真正可规模化交付的交易底座。</p>
<p>对我来说，这个项目最大的收获不是“做完了一次改版”，而是理解了一个核心事实：</p>
<p><strong>真正有价值的系统，不是把一个需求做完，而是把一类业务能力沉淀成平台。</strong></p>
<p>如果后续还要继续写下一篇，我会重点展开这个话题：</p>
<p><strong>《RFQ Convert 参数统一建模：如何把 from/to 语义映射成可复用的交易执行模型》</strong></p>
<h1>从输入到成交：我如何把 RFQ 从站点功能升级成 Global 级交易能力平台</h1>
<p>RFQ 是一种面向机构和大额交易用户的询价交易模式，通过聚合多做市商报价，为用户提供确定性价格和更优流动性。其核心流程包括用户准入（KYC/等级）、询价请求、做市商报价、报价聚合、用户确认以及基于OTC的清算结算。同时，RFQ系统通常与FX换汇、用户风控体系以及定投服务深度联动，形成完整的交易闭环</p>
<p><img decoding="async" loading="lazy" src="https://cdn.nlark.com/yuque/0/2026/png/1554318/1774247528032-e9672edb-ee9a-4044-9510-f119d3278730.png" alt="" class="img_OpE3"></p>
<p><img decoding="async" loading="lazy" src="https://cdn.nlark.com/yuque/0/2026/png/1554318/1774247740140-9e6bb1aa-ac8e-4b30-941d-e50352d32d70.png" alt="" class="img_OpE3"></p>
<h1></h1>
<p>这不是一篇“组件怎么写”的前端总结，而是一篇更适合面试、晋升答辩、项目复盘的业务型文章。
我想讲清楚的是：RFQ 为什么重要、原来为什么难做、我做了哪些关键改造、它给业务和团队带来了什么价值，以及这套能力未来还能往哪里走。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="目录">目录<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E7%9B%AE%E5%BD%95" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<ol>
<li>为什么 RFQ 值得单独做成公司级能力</li>
<li>项目背景：RFQ 早期到底卡在哪里</li>
<li>项目目标：不是做个页面，而是搭一套可复制的交易底座</li>
<li>项目产出的直接价值</li>
<li>RFQ 全链路拆解：从交易入口到成交回执</li>
<li>后端核心时序：quote 是怎么生成的，order 是怎么执行的</li>
<li>为什么这个项目难做：业务难点与技术难点</li>
<li>为什么这个项目有价值：业务痛点与改造收益</li>
<li>项目执行过程与阶段性落地</li>
<li>项目结果：业务价值、效率价值、技术价值</li>
<li>这段经历在面试里应该怎么讲</li>
<li>项目未来：从内部交易系统走向白标 SaaS 能力</li>
</ol>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-为什么-rfq-值得单独做成公司级能力">1) 为什么 RFQ 值得单独做成公司级能力<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#1-%E4%B8%BA%E4%BB%80%E4%B9%88-rfq-%E5%80%BC%E5%BE%97%E5%8D%95%E7%8B%AC%E5%81%9A%E6%88%90%E5%85%AC%E5%8F%B8%E7%BA%A7%E8%83%BD%E5%8A%9B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>RFQ（Request For Quote）不是普通的下单页，它是一个高度业务化的交易系统能力。</p>
<p>用户表面上只看到了一个简单流程：</p>
<ol>
<li>选择交易对</li>
<li>输入金额或数量</li>
<li>获取报价</li>
<li>确认成交</li>
<li>查看订单结果</li>
</ol>
<p>但在系统内部，这背后同时串联了：</p>
<ul>
<li>用户身份与 KYC 校验</li>
<li>交易权限与合规控制</li>
<li>实时报价与流动性路由</li>
<li>订单创建与状态流转</li>
<li>资产冻结与资产更新</li>
<li>多站点、多币种、多法币规则适配</li>
<li>活动、定投、FX 等业务能力复用</li>
</ul>
<p>所以 RFQ 不是“一个交易页面”，而是 ​<strong>交易能力、风控能力、资产能力、合规能力、站点能力的交汇点</strong>​。
一旦这条链路做得足够稳定、足够抽象，它就不再只是服务某一个页面，而会成为 Global、HK、ID 多市场共享的核心能力底座。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-项目背景rfq-早期到底卡在哪里">2) 项目背景：RFQ 早期到底卡在哪里<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#2-%E9%A1%B9%E7%9B%AE%E8%83%8C%E6%99%AFrfq-%E6%97%A9%E6%9C%9F%E5%88%B0%E5%BA%95%E5%8D%A1%E5%9C%A8%E5%93%AA%E9%87%8C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>RFQ 作为公司 Global 体系中的核心交易能力，在早期并不是一个真正的平台化系统，而更像是一组“为单一站点快速拼出来的定制功能”。</p>
<p>当时主要有 4 类问题。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="21-站点定制化严重复用能力弱">2.1 站点定制化严重，复用能力弱<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#21-%E7%AB%99%E7%82%B9%E5%AE%9A%E5%88%B6%E5%8C%96%E4%B8%A5%E9%87%8D%E5%A4%8D%E7%94%A8%E8%83%BD%E5%8A%9B%E5%BC%B1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>香港站、全球站、印尼站在业务规则、交易对、法币、合规要求上都有差异</li>
<li>早期实现更偏“站点单独适配”</li>
<li>每次开新站、上新玩法，都需要重新拼流程、重新接接口、重新做风控兜底</li>
</ul>
<p>结果就是：</p>
<ul>
<li>新站点上线周期长</li>
<li>维护成本高</li>
<li>逻辑容易分叉</li>
<li>一旦核心链路有调整，需要多处同步改</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="22-交易链路长用户决策成本高">2.2 交易链路长，用户决策成本高<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#22-%E4%BA%A4%E6%98%93%E9%93%BE%E8%B7%AF%E9%95%BF%E7%94%A8%E6%88%B7%E5%86%B3%E7%AD%96%E6%88%90%E6%9C%AC%E9%AB%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>RFQ 是即时报价交易，用户对“快”和“确定性”的敏感度非常高。
如果流程冗长、提示割裂、状态不清晰，就很容易发生：</p>
<ul>
<li>用户不知道当前价格是不是可执行价格</li>
<li>用户点了按钮后不知道系统处于哪一步</li>
<li>价格过期、余额不足、KYC 限制等错误在错误的时机才暴露</li>
</ul>
<p>本质上，这种问题会直接伤害成交转化率。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="23-业务扩展受限难支撑-fx定投等新形态">2.3 业务扩展受限，难支撑 FX、定投等新形态<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#23-%E4%B8%9A%E5%8A%A1%E6%89%A9%E5%B1%95%E5%8F%97%E9%99%90%E9%9A%BE%E6%94%AF%E6%92%91-fx%E5%AE%9A%E6%8A%95%E7%AD%89%E6%96%B0%E5%BD%A2%E6%80%81" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>RFQ 不只是简单的买卖币能力，随着业务发展，它开始承接：</p>
<ul>
<li>FX 外汇交易</li>
<li>定投交易</li>
<li>Stable 类营销与收益活动</li>
<li>多站点扩张</li>
</ul>
<p>如果底层交易模型不统一，后续每增加一个业务形态，都会变成一次新的“局部开发”，系统很快就会进入不可控状态。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="24-缺少统一的数据指标与效果评估体系">2.4 缺少统一的数据指标与效果评估体系<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#24-%E7%BC%BA%E5%B0%91%E7%BB%9F%E4%B8%80%E7%9A%84%E6%95%B0%E6%8D%AE%E6%8C%87%E6%A0%87%E4%B8%8E%E6%95%88%E6%9E%9C%E8%AF%84%E4%BC%B0%E4%BD%93%E7%B3%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>团队很难回答这些关键问题：</p>
<ul>
<li>询价成功率到底是多少</li>
<li>哪一步流失最严重</li>
<li>下单失败主要集中在哪些错误码</li>
<li>哪次流程优化到底带来了多少业务改善</li>
</ul>
<p>没有数据闭环，就很难从“主观感觉优化”走向“数据驱动优化”。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-项目目标不是做个页面而是搭一套可复制的交易底座">3) 项目目标：不是做个页面，而是搭一套可复制的交易底座<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#3-%E9%A1%B9%E7%9B%AE%E7%9B%AE%E6%A0%87%E4%B8%8D%E6%98%AF%E5%81%9A%E4%B8%AA%E9%A1%B5%E9%9D%A2%E8%80%8C%E6%98%AF%E6%90%AD%E4%B8%80%E5%A5%97%E5%8F%AF%E5%A4%8D%E5%88%B6%E7%9A%84%E4%BA%A4%E6%98%93%E5%BA%95%E5%BA%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这个项目的目标，不是简单优化某一版 RFQ 页面，而是围绕 RFQ 全链路完成一次平台化升级。</p>
<p>我把它拆成 5 个明确目标：</p>
<ol>
<li>打造可复用、可扩展的 RFQ 交易能力</li>
<li>优化交易体验，提高询价成功率与成交转化率</li>
<li>支撑 FX、定投等多业务形态发展</li>
<li>实现多站点快速复制，降低交付成本</li>
<li>建立数据驱动体系，支撑持续优化</li>
</ol>
<p>如果用一句面试化的话来概括，这个项目做的是：</p>
<p><strong>把一个站点级交易功能，升级成公司级可复用的交易能力平台。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-项目产出的直接价值">4) 项目产出的直接价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#4-%E9%A1%B9%E7%9B%AE%E4%BA%A7%E5%87%BA%E7%9A%84%E7%9B%B4%E6%8E%A5%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>团队围绕 RFQ 交易能力，最终完成了覆盖香港站、全球站、印尼站的多站点建设；落地了 RFQ、FX、定投等多种交易形态；打通从交易入口、实时报价、确认成交、订单管理到资产与合规提示的完整链路；将复杂的合规与交易能力规模化交付到不同市场，直接支撑公司多区域业务开展与实际收入增长。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="41-打造-global-级核心交易能力">4.1 打造 Global 级核心交易能力<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#41-%E6%89%93%E9%80%A0-global-%E7%BA%A7%E6%A0%B8%E5%BF%83%E4%BA%A4%E6%98%93%E8%83%BD%E5%8A%9B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>RFQ 作为 Global 站最重要的交易能力之一，通过架构梳理与流程重构，从站点定制实现升级为可复用、可扩展的交易能力平台</li>
<li>RFQ 能力快速复用至印尼站并稳定运行，显著降低新站点交易能力建设成本</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="42-交易体验与业务指标显著提升">4.2 交易体验与业务指标显著提升<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#42-%E4%BA%A4%E6%98%93%E4%BD%93%E9%AA%8C%E4%B8%8E%E4%B8%9A%E5%8A%A1%E6%8C%87%E6%A0%87%E6%98%BE%E8%91%97%E6%8F%90%E5%8D%87" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>通过“询价前置 + 流程简化 + 交易链路优化”，RFQ 交易从“可用”升级为“高效、稳定、可规模化放量”的核心交易能力。</p>
<p>这类优化直接带来三类收益：</p>
<ul>
<li>降低用户交易决策成本</li>
<li>提升询价成功率与下单效率</li>
<li>降低交易中断与无效操作比例</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="43-数据驱动优化形成可量化增长闭环">4.3 数据驱动优化，形成可量化增长闭环<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#43-%E6%95%B0%E6%8D%AE%E9%A9%B1%E5%8A%A8%E4%BC%98%E5%8C%96%E5%BD%A2%E6%88%90%E5%8F%AF%E9%87%8F%E5%8C%96%E5%A2%9E%E9%95%BF%E9%97%AD%E7%8E%AF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>建立 RFQ 关键指标统计与分析体系</li>
<li>可以持续跟踪询价、确认、下单、执行等关键节点</li>
<li>为后续交易能力持续优化提供数据决策基础</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-rfq-全链路拆解从交易入口到成交回执">5) RFQ 全链路拆解：从交易入口到成交回执<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#5-rfq-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%8B%86%E8%A7%A3%E4%BB%8E%E4%BA%A4%E6%98%93%E5%85%A5%E5%8F%A3%E5%88%B0%E6%88%90%E4%BA%A4%E5%9B%9E%E6%89%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果从用户视角看，RFQ 的完整链路可以拆成 7 步。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="51-交易入口">5.1 交易入口<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#51-%E4%BA%A4%E6%98%93%E5%85%A5%E5%8F%A3" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>交易入口并不只来自一个页面，它通常分布在多个流量来源中：</p>
<ul>
<li>首页 Banner 导流</li>
<li>行情页 / Price 页跳转</li>
<li>历史交易页返回</li>
<li>某些活动或定投关联入口</li>
</ul>
<p>在当前项目中，对外很多入口仍然叫 <code>rfq</code>，但实际承载已经统一收敛到 <code>flash</code> 路由。
这一步的意义并不只是路由重定向，而是 ​<strong>前台入口统一、后台能力统一</strong>​。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="52-初始化交易上下文">5.2 初始化交易上下文<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#52-%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%A4%E6%98%93%E4%B8%8A%E4%B8%8B%E6%96%87" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>用户进入页面后，系统需要先完成一轮交易上下文准备：</p>
<ul>
<li>解析 URL 中的默认币对</li>
<li>获取支持交易的币种、法币、交易对关系</li>
<li>获取交易精度、最小值、最大值、交易状态</li>
<li>读取用户最近一次成功交易的币对偏好</li>
<li>初始化当前 tab：<code>BUY / SELL / CONVERT</code></li>
</ul>
<p>这一层决定了页面是不是“可交易”，也决定了后续参数映射是不是正确。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="53-实时询价">5.3 实时询价<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#53-%E5%AE%9E%E6%97%B6%E8%AF%A2%E4%BB%B7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>当用户输入金额或数量后，系统不会等到最后一步才报价，而是先做一轮实时预询价。</p>
<p>这一步的业务意义很大：</p>
<ul>
<li>让用户先看到真实可参考的价格</li>
<li>在输入阶段尽早暴露不合法金额、精度或限额问题</li>
<li>提前缩短用户的决策路径</li>
</ul>
<p>从体验上看，这一步只是页面上实时展示汇率与倒计时；
从业务上看，这一步相当于把“价格确认”前置了。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="54-正式询价">5.4 正式询价<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#54-%E6%AD%A3%E5%BC%8F%E8%AF%A2%E4%BB%B7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>用户点击 <code>Get Quote</code> 后，系统发起正式询价，拿到：</p>
<ul>
<li><code>quoteId</code></li>
<li><code>quotePrice</code></li>
<li><code>quoteQuantity</code></li>
<li><code>quoteTotalAmount</code></li>
<li><code>ttl</code> 或等效报价有效时间</li>
</ul>
<p>这一阶段的关键不是“再调一次接口”，而是：</p>
<ul>
<li>把展示级报价切换成可执行报价</li>
<li>将这次报价纳入一个可下单、可追踪、可过期控制的正式交易上下文</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="55-报价确认">5.5 报价确认<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#55-%E6%8A%A5%E4%BB%B7%E7%A1%AE%E8%AE%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>用户进入确认页后，页面会展示：</p>
<ul>
<li>买卖双方币种</li>
<li>支付金额 / 接收金额</li>
<li>执行价格</li>
<li>报价倒计时</li>
<li>零手续费或活动权益提示</li>
</ul>
<p>这一步是整个 RFQ 的核心信任节点。
因为用户不是“盲下单”，而是在明确确认当前成交条件后才点击确认。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="56-下单与执行">5.6 下单与执行<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#56-%E4%B8%8B%E5%8D%95%E4%B8%8E%E6%89%A7%E8%A1%8C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>当用户点击 <code>Confirm</code> 后，系统进入真正的交易执行阶段：</p>
<ol>
<li>使用 <code>quoteId</code> 发起下单</li>
<li>服务端校验 quote 是否仍有效</li>
<li>校验用户资格、风控规则、订单幂等</li>
<li>冻结对应资产</li>
<li>异步向流动性侧或 MM 发起对冲 / 执行</li>
<li>更新订单状态</li>
<li>前端轮询订单执行结果</li>
</ol>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="57-结果回传与后续动作">5.7 结果回传与后续动作<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#57-%E7%BB%93%E6%9E%9C%E5%9B%9E%E4%BC%A0%E4%B8%8E%E5%90%8E%E7%BB%AD%E5%8A%A8%E4%BD%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>如果成功：</p>
<ul>
<li>展示成交结果页</li>
<li>更新用户资产</li>
<li>可引导到定投、划转、后续交易等页面</li>
</ul>
<p>如果失败：</p>
<ul>
<li>展示失败原因</li>
<li>允许用户回到交易页重新发起流程</li>
</ul>
<p>这意味着 RFQ 不只是“下单”，它还承担了 <strong>成交回执、资产反馈、后续业务转化</strong> 三个职责。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="6-后端核心时序quote-是怎么生成的order-是怎么执行的">6) 后端核心时序：quote 是怎么生成的，order 是怎么执行的<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#6-%E5%90%8E%E7%AB%AF%E6%A0%B8%E5%BF%83%E6%97%B6%E5%BA%8Fquote-%E6%98%AF%E6%80%8E%E4%B9%88%E7%94%9F%E6%88%90%E7%9A%84order-%E6%98%AF%E6%80%8E%E4%B9%88%E6%89%A7%E8%A1%8C%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>前端看到的是一个交易页面，后端实际上跑的是一套多系统协作链路。
结合后端时序，可以把 RFQ 拆成“报价链路”和“执行链路”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="61-报价链路quote-是怎么出来的">6.1 报价链路：quote 是怎么出来的<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#61-%E6%8A%A5%E4%BB%B7%E9%93%BE%E8%B7%AFquote-%E6%98%AF%E6%80%8E%E4%B9%88%E5%87%BA%E6%9D%A5%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>报价不是简单读一个行情价格返回给前端，而是一个带合规、汇率、MM 价格、缓存的组合过程。</p>
<p>大致过程如下：</p>
<ol>
<li>用户发起某个币种与法币的询价，例如数币/IDR</li>
<li>RFQ 服务生成 <code>quoteId</code></li>
<li>查询用户信息，获取：</li>
</ol>
<ul>
<li>KYC 状态</li>
<li>交易权限</li>
<li>用户等级</li>
</ul>
<ol start="4">
<li>查询当前交易对的行情价格</li>
<li>校验用户权限与币对交易状态</li>
<li>根据输入类型计算：</li>
</ol>
<ul>
<li>币数量</li>
<li>法币金额</li>
<li>对应价格</li>
</ul>
<ol start="7">
<li>查询法币与 USD 的汇率，例如 <code>IDR/USD</code></li>
<li>基于数币/USD 与法币/USD 汇率，计算对应法币报价</li>
<li>向 MM 侧获取或校准初始报价</li>
<li>计算最终对用户展示的成交价格</li>
<li>把 <code>quoteInfo</code> 存入 Redis</li>
<li>把法币/USD 汇率等关键上下文与 <code>quoteId</code> 关联缓存</li>
<li>将报价返回给前端</li>
</ol>
<p>这条链路说明了一个很重要的点：</p>
<p>RFQ 的 quote 不是一个“单纯的价格结果”，而是一个“带业务上下文、带时效、可下单、可追踪”的交易快照。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="62-执行链路order-是怎么完成的">6.2 执行链路：order 是怎么完成的<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#62-%E6%89%A7%E8%A1%8C%E9%93%BE%E8%B7%AForder-%E6%98%AF%E6%80%8E%E4%B9%88%E5%AE%8C%E6%88%90%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>当用户点击确认后，系统不是直接成交，而是进入订单执行状态机。</p>
<p>核心步骤如下：</p>
<ol>
<li>从 Redis 取回 <code>QuoteDetail</code></li>
<li>从 Redis 取回与该 quote 绑定的汇率上下文，例如 <code>IDR/USD</code></li>
<li>做幂等与有效性检查</li>
<li>创建订单并记录关键汇率字段</li>
<li>写入订单状态流转记录</li>
<li>做分布式锁控制，避免重复执行</li>
<li>冻结用户资产</li>
<li>异步向 spot / MM 发起执行或对冲</li>
<li>轮询 MM 结果</li>
<li>如果成功，更新订单状态并继续写入状态流转记录</li>
<li>前端轮询订单结果并展示成功或失败页</li>
</ol>
<p>这条链路的核心价值是：</p>
<ul>
<li>保证订单执行具备时效一致性</li>
<li>保证报价与下单使用的是同一份业务快照</li>
<li>保证资产冻结、状态流转、外部执行三者可追踪</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="7-为什么这个项目难做业务难点与技术难点">7) 为什么这个项目难做：业务难点与技术难点<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#7-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%99%E4%B8%AA%E9%A1%B9%E7%9B%AE%E9%9A%BE%E5%81%9A%E4%B8%9A%E5%8A%A1%E9%9A%BE%E7%82%B9%E4%B8%8E%E6%8A%80%E6%9C%AF%E9%9A%BE%E7%82%B9" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这类项目的难，不在“页面多复杂”，而在“每一步都牵涉多个业务系统，且任何一步出错都会直接影响交易结果”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="71-难点一参数语义复杂尤其是-convert">7.1 难点一：参数语义复杂，尤其是 Convert<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#71-%E9%9A%BE%E7%82%B9%E4%B8%80%E5%8F%82%E6%95%B0%E8%AF%AD%E4%B9%89%E5%A4%8D%E6%9D%82%E5%B0%A4%E5%85%B6%E6%98%AF-convert" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>Buy、Sell 看起来相对清晰，但 Convert 会引入一个非常典型的问题：</p>
<ul>
<li>用户理解的是 <code>from -&gt; to</code></li>
<li>系统执行的是 <code>tradeCurrency + settlementCurrency + side</code></li>
</ul>
<p>也就是说，前端看到的输入方向，并不等于后端实际执行方向。
当交易对只存在单向时，系统还需要动态判断应该走 Buy 还是 Sell 语义。</p>
<p>这件事难的地方不是写 if-else，而是：</p>
<ul>
<li>任何一处映射不一致，都会造成报价展示正确、下单参数错误</li>
<li>同一套页面逻辑要兼容 Buy/Sell/Convert 多种模式</li>
<li>还要适配 FX、定投等业务继续复用</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="72-难点二时效性与一致性同时要求很高">7.2 难点二：时效性与一致性同时要求很高<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#72-%E9%9A%BE%E7%82%B9%E4%BA%8C%E6%97%B6%E6%95%88%E6%80%A7%E4%B8%8E%E4%B8%80%E8%87%B4%E6%80%A7%E5%90%8C%E6%97%B6%E8%A6%81%E6%B1%82%E5%BE%88%E9%AB%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>RFQ 不是长期有效价格，quote 天生有 TTL。
一旦用户停留时间较长，就会出现“用户看到的价格”和“系统可执行价格”之间的时间差。</p>
<p>所以系统必须处理好：</p>
<ul>
<li>预询价与正式询价的衔接</li>
<li>确认页的报价刷新</li>
<li>下单前 quote 过期检查</li>
<li>重新询价后的状态一致性</li>
</ul>
<p>这类问题如果处理不好，最常见后果就是：</p>
<ul>
<li>用户频繁下单失败</li>
<li>用户感知价格跳动大</li>
<li>投诉价格不一致</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="73-难点三风控门禁不是一个点而是一条链">7.3 难点三：风控门禁不是一个点，而是一条链<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#73-%E9%9A%BE%E7%82%B9%E4%B8%89%E9%A3%8E%E6%8E%A7%E9%97%A8%E7%A6%81%E4%B8%8D%E6%98%AF%E4%B8%80%E4%B8%AA%E7%82%B9%E8%80%8C%E6%98%AF%E4%B8%80%E6%9D%A1%E9%93%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>RFQ 会同时受这些因素约束：</p>
<ul>
<li>是否登录</li>
<li>是否完成 KYC</li>
<li>BOA / POA 等合规审核状态</li>
<li>是否具备该币对交易权限</li>
<li>最小 / 最大订单限制</li>
<li>精度是否合法</li>
<li>余额是否足够</li>
<li>币对交易状态是否开放</li>
</ul>
<p>这意味着风控不是某个弹窗，而是一套贯穿“输入 -&gt; 报价 -&gt; 下单 -&gt; 执行”的门禁系统。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="74-难点四多站点多法币多业务复用">7.4 难点四：多站点、多法币、多业务复用<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#74-%E9%9A%BE%E7%82%B9%E5%9B%9B%E5%A4%9A%E7%AB%99%E7%82%B9%E5%A4%9A%E6%B3%95%E5%B8%81%E5%A4%9A%E4%B8%9A%E5%8A%A1%E5%A4%8D%E7%94%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>站点一多，复杂度会指数级上升。</p>
<p>因为不同站点会有不同：</p>
<ul>
<li>支持法币</li>
<li>合规规则</li>
<li>活动策略</li>
<li>页面入口</li>
<li>本地化文案</li>
<li>交易玩法</li>
</ul>
<p>如果系统是站点定制模式，那每开一个市场都像重做一次 RFQ。
而如果你想做成平台能力，就必须把“站点差异”沉淀成“规则与配置”，而不是“复制代码”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="75-难点五前端状态天然容易失控">7.5 难点五：前端状态天然容易失控<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#75-%E9%9A%BE%E7%82%B9%E4%BA%94%E5%89%8D%E7%AB%AF%E7%8A%B6%E6%80%81%E5%A4%A9%E7%84%B6%E5%AE%B9%E6%98%93%E5%A4%B1%E6%8E%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>前端这条链路里同时存在：</p>
<ul>
<li>tab 状态</li>
<li>币对状态</li>
<li>输入状态</li>
<li>fast quote 状态</li>
<li>quote confirm 状态</li>
<li>order result 状态</li>
<li>资产信息</li>
<li>用户信息</li>
<li>本地缓存</li>
</ul>
<p>再叠加防抖、轮询、异步接口、切换交易对，就很容易出现状态覆盖、旧请求污染新结果、边界条件难兜底的问题。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="8-为什么这个项目有价值业务痛点与改造收益">8) 为什么这个项目有价值：业务痛点与改造收益<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#8-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%99%E4%B8%AA%E9%A1%B9%E7%9B%AE%E6%9C%89%E4%BB%B7%E5%80%BC%E4%B8%9A%E5%8A%A1%E7%97%9B%E7%82%B9%E4%B8%8E%E6%94%B9%E9%80%A0%E6%94%B6%E7%9B%8A" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>项目真正有说服力的地方，不是“我重构了代码”，而是“我解决了什么业务痛点，并让业务更容易增长”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="81-痛点一早期-rfq-只能算能用不能算可放量">8.1 痛点一：早期 RFQ 只能算能用，不能算可放量<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#81-%E7%97%9B%E7%82%B9%E4%B8%80%E6%97%A9%E6%9C%9F-rfq-%E5%8F%AA%E8%83%BD%E7%AE%97%E8%83%BD%E7%94%A8%E4%B8%8D%E8%83%BD%E7%AE%97%E5%8F%AF%E6%94%BE%E9%87%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>早期 RFQ 的问题不是不能交易，而是：</p>
<ul>
<li>流程割裂</li>
<li>体验不稳定</li>
<li>复用成本高</li>
<li>交易链路容易中断</li>
</ul>
<p>这类系统在业务初期还能顶住，但一旦要做 Global 主交易能力，就会暴露出明显瓶颈。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="82-痛点二每个新业务都在重复造轮子">8.2 痛点二：每个新业务都在“重复造轮子”<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#82-%E7%97%9B%E7%82%B9%E4%BA%8C%E6%AF%8F%E4%B8%AA%E6%96%B0%E4%B8%9A%E5%8A%A1%E9%83%BD%E5%9C%A8%E9%87%8D%E5%A4%8D%E9%80%A0%E8%BD%AE%E5%AD%90" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>如果 RFQ 只是一个单点功能，那么：</p>
<ul>
<li>做 FX 需要再适配一套</li>
<li>做定投需要再接一套</li>
<li>做新站点还要再适配一轮</li>
</ul>
<p>长期来看，这种模式的成本不是开发成本，而是 ​<strong>组织协作成本和持续维护成本</strong>​。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="83-痛点三缺少数据闭环优化难以自证">8.3 痛点三：缺少数据闭环，优化难以自证<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#83-%E7%97%9B%E7%82%B9%E4%B8%89%E7%BC%BA%E5%B0%91%E6%95%B0%E6%8D%AE%E9%97%AD%E7%8E%AF%E4%BC%98%E5%8C%96%E9%9A%BE%E4%BB%A5%E8%87%AA%E8%AF%81" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>很多交易系统都会做优化，但如果没有指标体系，就只能停留在“感觉变好了”。
而交易业务最怕的就是：</p>
<ul>
<li>不知道哪一步流失最严重</li>
<li>不知道失败码主要集中在哪</li>
<li>不知道流程优化有没有真正提高成交效率</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="84-这次改造带来的核心收益">8.4 这次改造带来的核心收益<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#84-%E8%BF%99%E6%AC%A1%E6%94%B9%E9%80%A0%E5%B8%A6%E6%9D%A5%E7%9A%84%E6%A0%B8%E5%BF%83%E6%94%B6%E7%9B%8A" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这次 RFQ 项目之后，团队获得的不是“一个新版页面”，而是三层收益。</p>
<p>第一层是业务收益：</p>
<ul>
<li>RFQ 交易量显著提升</li>
<li>询价成功率与成交转化率明显提升</li>
<li>RFQ 成为支撑 Global、ID、HK 的重要交易基础能力</li>
</ul>
<p>第二层是组织效率收益：</p>
<ul>
<li>新站点 RFQ 能力建设周期大幅缩短</li>
<li>复用 Global 成熟能力，减少重复开发与维护成本</li>
<li>交易链路稳定后，运营与问题处理成本显著下降</li>
</ul>
<p>第三层是技术资产收益：</p>
<ul>
<li>一套核心系统支撑多区域、多业务</li>
<li>统一交易模型与技术标准</li>
<li>为 FX、定投以及后续新型交易产品提供基础能力</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="9-项目执行过程与阶段性落地">9) 项目执行过程与阶段性落地<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#9-%E9%A1%B9%E7%9B%AE%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E4%B8%8E%E9%98%B6%E6%AE%B5%E6%80%A7%E8%90%BD%E5%9C%B0" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这个项目不是一次性重写完成的，而是随着业务节奏逐步推进的。</p>
<table><thead><tr><th><strong>时间</strong></th><th><strong>项目举措</strong></th><th><strong>详细描述</strong></th></tr></thead><tbody><tr><td>2025.07.22</td><td>HK RFQ 全新改版</td><td>询价前置，在下单前完成报价判断，减少无效操作</td></tr><tr><td>2025.08.13</td><td>Global RFQ 定投上线</td><td>拓展定投交易形态</td></tr><tr><td>2025.09.08</td><td>HK FX 上线</td><td>支持外汇交易场景</td></tr><tr><td>2025.09.24</td><td>HK RFQ 定投上线</td><td>定投能力复用，多业务能力落地</td></tr><tr><td>2025.10.28</td><td>Global RFQ 流程优化</td><td>简化交易步骤，减少用户等待时间与中断点，提升整体交易流畅度</td></tr><tr><td>2025.10.28</td><td>Global RFQ 改版升级</td><td>对 Global 站 RFQ 核心链路进行系统性重构，提升可维护性与扩展性</td></tr><tr><td>2025.11.26</td><td>ID RFQ 快速复用</td><td>快速支持印尼站上线</td></tr><tr><td>2025.12.03</td><td>HK RFQ 迁移与优化</td><td>完成 RFQ 能力迁移并统一交易模型，支持 FX 与定投等多业务场景</td></tr><tr><td>2025.12.15</td><td>数据统计与效果评估</td><td>建立 RFQ 关键指标体系，持续评估优化效果</td></tr></tbody></table>
<p>如果把这些动作抽象一下，会发现项目推进路径非常清晰：</p>
<ol>
<li>先优化核心链路体验</li>
<li>再扩展多业务玩法</li>
<li>再做 Global 化与平台化升级</li>
<li>最后补齐数据闭环与效果评估</li>
</ol>
<p>这也是一个很适合在面试里讲的“从战术优化走向战略能力建设”的项目路径。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="10-项目结果业务价值效率价值技术价值">10) 项目结果：业务价值、效率价值、技术价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#10-%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%9C%E4%B8%9A%E5%8A%A1%E4%BB%B7%E5%80%BC%E6%95%88%E7%8E%87%E4%BB%B7%E5%80%BC%E6%8A%80%E6%9C%AF%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="101-业务价值">10.1 业务价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#101-%E4%B8%9A%E5%8A%A1%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>RFQ 交易量显著提升，成为 Global 主交易能力中的持续放量模块</li>
<li>询价成功率与成交转化率明显提升</li>
<li>RFQ 成为支撑 Global、ID、HK 的核心交易基础能力</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="102-效率价值">10.2 效率价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#102-%E6%95%88%E7%8E%87%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>新站点 RFQ 能力建设周期大幅缩短</li>
<li>复用 Global 成熟能力，减少重复开发与维护成本</li>
<li>链路稳定后，运营与问题处理成本显著下降</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="103-技术价值">10.3 技术价值<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#103-%E6%8A%80%E6%9C%AF%E4%BB%B7%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>高稳定性：</p>
<ul>
<li>多站点高并发场景下稳定运行</li>
<li>核心链路可靠性持续提升</li>
</ul>
<p>平台化与复用能力：</p>
<ul>
<li>一套核心系统支撑多区域、多业务</li>
<li>统一交易模型与技术标准</li>
</ul>
<p>可持续演进：</p>
<ul>
<li>支持 FX、定投等持续扩展</li>
<li>为后续新型交易产品提供基础能力</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="11-这段经历在面试里应该怎么讲">11) 这段经历在面试里应该怎么讲<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#11-%E8%BF%99%E6%AE%B5%E7%BB%8F%E5%8E%86%E5%9C%A8%E9%9D%A2%E8%AF%95%E9%87%8C%E5%BA%94%E8%AF%A5%E6%80%8E%E4%B9%88%E8%AE%B2" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果你要把这个项目讲得“既懂业务又懂系统”，建议不要只讲“我做了一个 RFQ 页面”，而要按下面这个结构来讲。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="111-先讲项目级别">11.1 先讲项目级别<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#111-%E5%85%88%E8%AE%B2%E9%A1%B9%E7%9B%AE%E7%BA%A7%E5%88%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>可以先定调：</p>
<p>我做的不是单一页面优化，而是围绕 RFQ 核心交易链路做了一次平台化升级，目标是让这套能力可以支撑 Global、HK、ID 多站点以及 FX、定投等多业务形态。</p>
<p>这句话能直接把项目从“需求开发”拉升到“核心能力建设”。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="112-再讲你解决的核心矛盾">11.2 再讲你解决的核心矛盾<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#112-%E5%86%8D%E8%AE%B2%E4%BD%A0%E8%A7%A3%E5%86%B3%E7%9A%84%E6%A0%B8%E5%BF%83%E7%9F%9B%E7%9B%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>建议用“旧系统为什么不行”来讲，而不是直接堆功能点：</p>
<ul>
<li>旧系统站点定制化严重，复用能力差</li>
<li>交易链路长，用户决策成本高</li>
<li>参数语义复杂，尤其是 Convert 映射容易出错</li>
<li>缺少指标体系，优化难以闭环</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="113-然后讲你的关键动作">11.3 然后讲你的关键动作<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#113-%E7%84%B6%E5%90%8E%E8%AE%B2%E4%BD%A0%E7%9A%84%E5%85%B3%E9%94%AE%E5%8A%A8%E4%BD%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>你可以把动作概括为 4 个关键词：</p>
<ul>
<li>流程前置：把询价前置，减少无效操作</li>
<li>模型统一：统一交易模型和参数语义</li>
<li>平台复用：让能力可支撑多站点、多业务</li>
<li>数据闭环：建立关键指标统计与效果评估体系</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="114-最后讲结果">11.4 最后讲结果<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#114-%E6%9C%80%E5%90%8E%E8%AE%B2%E7%BB%93%E6%9E%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>结果不要只讲“上线了”，而要讲三类结果：</p>
<ul>
<li>业务结果：交易量、询价成功率、转化率提升</li>
<li>效率结果：新站点接入更快、复用成本更低</li>
<li>技术结果：系统更稳定、更可扩展、更可持续演进</li>
</ul>
<p>如果面试官继续追问细节，再展开你对：</p>
<ul>
<li>quote 与 order 的两段式链路理解</li>
<li>Convert 参数映射</li>
<li>风控门禁前置</li>
<li>多站点差异抽象</li>
<li>数据指标设计</li>
</ul>
<p>这几部分的思考，基本就能把项目讲深了。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="12-项目未来从内部交易系统走向白标-saas-能力">12) 项目未来：从内部交易系统走向白标 SaaS 能力<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#12-%E9%A1%B9%E7%9B%AE%E6%9C%AA%E6%9D%A5%E4%BB%8E%E5%86%85%E9%83%A8%E4%BA%A4%E6%98%93%E7%B3%BB%E7%BB%9F%E8%B5%B0%E5%90%91%E7%99%BD%E6%A0%87-saas-%E8%83%BD%E5%8A%9B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>我对这个项目未来的理解，不只是继续做几个站点，而是把 RFQ 系统从内部工具升级为可对外输出的白标 SaaS 产品，实现降本增收与生态拓展。</p>
<p>整体可以分三阶段、约 18 个月推进。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第一阶段底座重构-1">第一阶段：底座重构<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E7%AC%AC%E4%B8%80%E9%98%B6%E6%AE%B5%E5%BA%95%E5%BA%A7%E9%87%8D%E6%9E%84-1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>目标是抽象参数配置、做配置化设计，并拆分报价、风控、结算等核心模块，逐步演进成更轻量、更标准化的服务。</p>
<p>预期收益：</p>
<ul>
<li>减少运营成本</li>
<li>提升复用性</li>
<li>提升稳定性与运维效率</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第二阶段业务扩容-1">第二阶段：业务扩容<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E7%AC%AC%E4%BA%8C%E9%98%B6%E6%AE%B5%E4%B8%9A%E5%8A%A1%E6%89%A9%E5%AE%B9-1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>在现货 RFQ 基础上，逐步纳入更多产品线：</p>
<ul>
<li>合约</li>
<li>OTC</li>
<li>理财</li>
</ul>
<p>最终形成统一报价、统一交易入口、统一撮合与风控逻辑的多产品 RFQ 平台。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第三阶段商业化与生态化-1">第三阶段：商业化与生态化<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E7%AC%AC%E4%B8%89%E9%98%B6%E6%AE%B5%E5%95%86%E4%B8%9A%E5%8C%96%E4%B8%8E%E7%94%9F%E6%80%81%E5%8C%96-1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>把 RFQ 能力标准化为多租户 SaaS 方案，通过订阅制加“流动性租户”模式，服务线上或线下的中小型 P2P OTC 业务服务商。</p>
<p>这一步一旦跑通，RFQ 的角色就会从：</p>
<ul>
<li>公司内部交易系统</li>
</ul>
<p>升级为：</p>
<ul>
<li>可输出的流动性与交易基础设施产品</li>
</ul>
<p>如果这条路能走通，它的价值就不仅仅是支撑公司现有业务，而是变成新的收入模型与生态能力。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="总结-1">总结<a href="https://blog.kuizuo.me/blog/rfq-platform-upgrade#%E6%80%BB%E7%BB%93-1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>RFQ 项目最值得讲的地方，不是页面有多复杂，而是它把交易、风控、资产、合规、站点扩张、业务复用、数据驱动这些原本分散的能力，整合成了一套真正可规模化交付的交易底座。</p>
<p>对我来说，这个项目最大的收获不是“做完了一次改版”，而是理解了一个核心事实：</p>
<p>真正有价值的系统，不是把一个需求做完，而是把一类业务能力沉淀成平台。</p>
<p>如果后续要继续写下一篇，我会重点展开这个话题：</p>
<p><strong>《RFQ Convert 参数统一建模：如何把 from/to 语义映射成可复用的交易执行模型》</strong></p>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="金融" term="金融"/>
        <category label="交易系统" term="交易系统"/>
        <category label="RFQ" term="RFQ"/>
        <category label="平台化" term="平台化"/>
        <category label="项目复盘" term="项目复盘"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[什么是网关（Gateway）]]></title>
        <id>https://blog.kuizuo.me/blog/api-gateway</id>
        <link href="https://blog.kuizuo.me/blog/api-gateway"/>
        <updated>2026-03-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[网关到底是什么？为什么几乎所有大型系统都需要网关？从统一入口、路由转发、认证鉴权到限流监控，一篇文章讲清网关的作用与架构位置。]]></summary>
        <content type="html"><![CDATA[<p><strong>从「多入口」到「统一入口 + 流量调度中心」</strong></p>
<p>要理解网关，先看它解决什么问题：微服务拆多了，客户端要面对一堆服务地址、一堆认证和限流逻辑，<strong>维护和安全都难</strong>。<strong>网关</strong>就是放在最前面的<strong>统一入口和调度层</strong>——所有请求先到网关，再由网关转发到具体服务。下面按「架构位置 → 核心能力 → 为什么需要 → 常见实现 → 总结」用总分总的方式讲清。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一网关在系统架构中的位置">一、网关在系统架构中的位置<a href="https://blog.kuizuo.me/blog/api-gateway#%E4%B8%80%E7%BD%91%E5%85%B3%E5%9C%A8%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E4%B8%AD%E7%9A%84%E4%BD%8D%E7%BD%AE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="先说一句网关--内网与外网的桥梁">先说一句：网关 = 内网与外网的桥梁<a href="https://blog.kuizuo.me/blog/api-gateway#%E5%85%88%E8%AF%B4%E4%B8%80%E5%8F%A5%E7%BD%91%E5%85%B3--%E5%86%85%E7%BD%91%E4%B8%8E%E5%A4%96%E7%BD%91%E7%9A%84%E6%A1%A5%E6%A2%81" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>在更广义的理解里，<strong>网关</strong>本身就是<strong>内网和外网之间的桥梁</strong>：内网设备通过网关才能访问外网，外网要访问内网或内网要对外暴露服务，也经过网关。网关可以同时做两件事：</p>
<ul>
<li><strong>让内网访问外网</strong>：内网主机把「默认网关」设为网关设备（如家庭里的路由器），所有访问互联网的流量先发到网关，再由网关转发出去。</li>
<li><strong>对外网流量做过滤、拦截</strong>：网关可以对进出的流量做<strong>防火墙</strong>策略——放行、限速、拦截恶意 IP、过滤敏感请求等，保护内网安全。</li>
</ul>
<p><strong>家庭网络</strong>就是典型例子：家里多台设备在<strong>内网</strong>（如 192.168.1.x），<strong>路由器</strong>通常就是网关（常见管理地址 192.168.1.1）。你上网时，流量先到路由器，再由它转发到运营商、再进互联网；路由器也可以做家长控制、访问限制等，本质上就是「流量过滤、拦截」。下文说的 <strong>API 网关</strong>，是在微服务/系统架构里的一种网关，负责「客户端（外）与后端服务（内）」之间的入口与调度，和「内网通过网关访外网、网关做防火墙」是同一类「内外桥梁」思路在不同场景的体现。</p>
<hr>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="系统架构中的网关单体-vs-微服务">系统架构中的网关：单体 vs 微服务<a href="https://blog.kuizuo.me/blog/api-gateway#%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E4%B8%AD%E7%9A%84%E7%BD%91%E5%85%B3%E5%8D%95%E4%BD%93-vs-%E5%BE%AE%E6%9C%8D%E5%8A%A1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p><strong>单体时代</strong>：客户端直接访问一台服务器，路径简单：Browser → Server → Database。</p>
<p><strong>微服务时代</strong>：系统拆成用户服务、订单服务、支付服务、商品服务等。若客户端直连每个服务，会变成：</p>
<ul>
<li>Browser → 用户服务</li>
<li>Browser → 订单服务</li>
<li>Browser → 支付服务</li>
<li>…</li>
</ul>
<p>带来的问题：</p>
<table><thead><tr><th>问题</th><th>说明</th></tr></thead><tbody><tr><td><strong>服务地址暴露</strong></td><td>所有服务域名/端口对前端或外部可见，难以收敛与变更</td></tr><tr><td><strong>客户端臃肿</strong></td><td>要配很多 API 地址、不同环境的差异全在客户端</td></tr><tr><td><strong>认证重复</strong></td><td>每个服务都要做 Token/JWT 校验，逻辑重复、易不一致</td></tr><tr><td><strong>难以统一管理</strong></td><td>限流、日志、监控分散，没有统一管控点</td></tr></tbody></table>
<p>于是在客户端与后端服务之间加一层<strong>网关（Gateway）</strong>：</p>
<div class="language-javascript codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-javascript codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token maybe-class-name">Browser</span><span class="token plain"> → </span><span class="token constant" style="color:hsl(35, 99%, 36%)">API</span><span class="token plain"> </span><span class="token maybe-class-name">Gateway</span><span class="token plain"> → 用户服务 </span><span class="token operator" style="color:hsl(221, 87%, 60%)">/</span><span class="token plain"> 订单服务 </span><span class="token operator" style="color:hsl(221, 87%, 60%)">/</span><span class="token plain"> 支付服务 </span><span class="token operator" style="color:hsl(221, 87%, 60%)">/</span><span class="token plain"> 商品服务</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>客户端只认一个入口（如 <code>https://api.example.com</code>），由网关按规则把请求<strong>转发</strong>到对应服务。可以类比：<strong>网关 = 城市大门 + 安检 + 交通调度</strong>，所有「进城」的请求都先经过这里。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二网关的核心能力">二、网关的核心能力<a href="https://blog.kuizuo.me/blog/api-gateway#%E4%BA%8C%E7%BD%91%E5%85%B3%E7%9A%84%E6%A0%B8%E5%BF%83%E8%83%BD%E5%8A%9B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>网关不只是「转发」，它通常承担系统的<strong>基础设施能力</strong>，可理解为「<strong>系统入口 + 请求调度中心</strong>」。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-统一入口">1. 统一入口<a href="https://blog.kuizuo.me/blog/api-gateway#1-%E7%BB%9F%E4%B8%80%E5%85%A5%E5%8F%A3" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>所有请求先进网关，再按路径等规则分发，例如：</p>
<ul>
<li><code>/api/user/*</code> → 用户服务</li>
<li><code>/api/order/*</code> → 订单服务</li>
<li><code>/api/pay/*</code> → 支付服务</li>
</ul>
<p><strong>好处</strong>：客户端只记一个域名；服务地址不暴露；结构清晰。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-路由转发">2. 路由转发<a href="https://blog.kuizuo.me/blog/api-gateway#2-%E8%B7%AF%E7%94%B1%E8%BD%AC%E5%8F%91" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>按<strong>路径、域名、Header</strong> 等规则把请求转到不同服务，类似<strong>快递分拣</strong>：包裹先到分拣中心，再按目的地分发。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-身份认证与权限控制">3. 身份认证与权限控制<a href="https://blog.kuizuo.me/blog/api-gateway#3-%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81%E4%B8%8E%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>在大系统里，<strong>认证往往收口到网关</strong>：Token 校验、JWT 验证、OAuth 等。请求先经网关验签，合法则转发，非法则直接 401，<strong>各业务服务不必重复写鉴权</strong>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-限流与防护">4. 限流与防护<a href="https://blog.kuizuo.me/blog/api-gateway#4-%E9%99%90%E6%B5%81%E4%B8%8E%E9%98%B2%E6%8A%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>为防止被恶意或突发流量打满，网关做<strong>限流</strong>（如每秒最多 1000 请求），常用<strong>令牌桶、漏桶</strong>等算法，应对爬虫、DDoS、流量尖刺。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-日志与监控">5. 日志与监控<a href="https://blog.kuizuo.me/blog/api-gateway#5-%E6%97%A5%E5%BF%97%E4%B8%8E%E7%9B%91%E6%8E%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>作为全流量入口，网关适合做<strong>统一日志</strong>：谁访问、什么路径、耗时、是否失败，用于性能监控、排障和用户行为分析。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="6-协议转换">6. 协议转换<a href="https://blog.kuizuo.me/blog/api-gateway#6-%E5%8D%8F%E8%AE%AE%E8%BD%AC%E6%8D%A2" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>客户端与后端协议不一致时（如前端 HTTP/REST，后端 gRPC），可在网关做<strong>协议转换</strong>，对前端透明。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="7-灰度发布与流量分配">7. 灰度发布与流量分配<a href="https://blog.kuizuo.me/blog/api-gateway#7-%E7%81%B0%E5%BA%A6%E5%8F%91%E5%B8%83%E4%B8%8E%E6%B5%81%E9%87%8F%E5%88%86%E9%85%8D" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>发布新版本时，网关可按比例分配流量（如 10% 走新版本、90% 走旧版本），实现<strong>灰度发布（Canary）</strong>，降低上线风险。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三为什么微服务一定需要网关">三、为什么微服务一定需要网关<a href="https://blog.kuizuo.me/blog/api-gateway#%E4%B8%89%E4%B8%BA%E4%BB%80%E4%B9%88%E5%BE%AE%E6%9C%8D%E5%8A%A1%E4%B8%80%E5%AE%9A%E9%9C%80%E8%A6%81%E7%BD%91%E5%85%B3" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>服务数量到几十、上百时，若没有网关：</p>
<ul>
<li>客户端要维护大量服务地址，<strong>复杂度高</strong>；</li>
<li>认证、限流、监控分散，<strong>安全与可观测性差</strong>；</li>
<li>任何服务地址或路由变更都要推给客户端，<strong>维护成本大</strong>。</li>
</ul>
<p>有网关后：<strong>Client → Gateway → Services</strong>，客户端只访问一个入口（如 <code>api.example.com</code>），复杂逻辑集中在网关。因此<strong>几乎所有大型互联网系统</strong>都会在微服务前加一层网关。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四常见的网关实现">四、常见的网关实现<a href="https://blog.kuizuo.me/blog/api-gateway#%E5%9B%9B%E5%B8%B8%E8%A7%81%E7%9A%84%E7%BD%91%E5%85%B3%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-nginx">1. Nginx<a href="https://blog.kuizuo.me/blog/api-gateway#1-nginx" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p><strong>Nginx</strong> 是很多系统的第一层入口，做<strong>反向代理、负载均衡、限流、HTTPS</strong>。不少公司直接用 Nginx 当网关，或 Nginx 前置、后面再接专业 API 网关。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-专业-api-网关">2. 专业 API 网关<a href="https://blog.kuizuo.me/blog/api-gateway#2-%E4%B8%93%E4%B8%9A-api-%E7%BD%91%E5%85%B3" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>在微服务架构中常用<strong>专用 API 网关</strong>，例如：</p>
<ul>
<li><strong>Kong</strong>、<strong>APISIX</strong>：开源，插件丰富，动态路由、认证、限流、监控</li>
<li><strong>Spring Cloud Gateway</strong>：与 Java/Spring 生态集成</li>
<li><strong>AWS API Gateway</strong>：云上托管，与 AWS 服务打通</li>
</ul>
<p>这类产品通常提供插件体系、动态路由、统一认证、限流与监控能力。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-bffbackend-for-frontend">3. BFF（Backend For Frontend）<a href="https://blog.kuizuo.me/blog/api-gateway#3-bffbackend-for-frontend" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p><strong>BFF</strong> 是一种「为前端定制的网关」：不同客户端（Web、App）用不同的 BFF 聚合接口、做数据裁剪，减少前端请求次数、简化前端逻辑。架构上可看作：Browser → Web BFF / Mobile BFF → 各微服务。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五总结">五、总结<a href="https://blog.kuizuo.me/blog/api-gateway#%E4%BA%94%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>说白了就四条：</p>
<ol>
<li><strong>网关是什么</strong>：系统的<strong>统一入口 + 请求调度中心</strong>，所有请求先经网关再进具体服务；可类比「城市大门 + 安检 + 交通调度」。</li>
<li><strong>核心能力</strong>：<strong>统一入口</strong>、<strong>路由转发</strong>、<strong>认证鉴权</strong>、<strong>限流防护</strong>、<strong>日志监控</strong>、<strong>协议转换</strong>、<strong>灰度/流量分配</strong>，把跨服务的公共能力收口到一层。</li>
<li><strong>为什么需要</strong>：微服务多了以后，没有网关则地址暴露、认证重复、难以限流与监控；有网关则客户端只认一个入口，复杂度与风险集中在网关，便于运维与安全。</li>
<li><strong>常见实现</strong>：<strong>Nginx</strong> 做反向代理与负载均衡；<strong>Kong/APISIX/Spring Cloud Gateway</strong> 等做专业 API 网关；<strong>BFF</strong> 为前端做聚合与裁剪。整体链路可简化为：<strong>客户端 → 网关 → 微服务 → 数据库</strong>，网关已成为微服务架构里<strong>不可缺少的一层基础设施</strong>。</li>
</ol>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="后端架构" term="后端架构"/>
        <category label="微服务" term="微服务"/>
        <category label="API Gateway" term="API Gateway"/>
        <category label="Nginx" term="Nginx"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[什么是区块链？]]></title>
        <id>https://blog.kuizuo.me/blog/what-is-blockchain</id>
        <link href="https://blog.kuizuo.me/blog/what-is-blockchain"/>
        <updated>2026-02-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[从中心化问题到去中心化解决方案——区块链要解决什么，以及共享账本、区块、链与防篡改如何实现。]]></summary>
        <content type="html"><![CDATA[<p><strong>从中心化问题到去中心化解决方案</strong></p>
<p>要真正理解区块链的本质，我们首先必须了解它旨在解决的根本问题。区块链技术的核心在于应对<strong>中心化系统固有的漏洞和局限性</strong>，这些漏洞和局限性体现在<strong>我们管理资金的方式以及达成集体协议的方式上</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一问题一个建立在中心化信任之上的世界">一、问题：一个建立在中心化信任之上的世界<a href="https://blog.kuizuo.me/blog/what-is-blockchain#%E4%B8%80%E9%97%AE%E9%A2%98%E4%B8%80%E4%B8%AA%E5%BB%BA%E7%AB%8B%E5%9C%A8%E4%B8%AD%E5%BF%83%E5%8C%96%E4%BF%A1%E4%BB%BB%E4%B9%8B%E4%B8%8A%E7%9A%84%E4%B8%96%E7%95%8C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>我们的现代世界依赖<strong>中介机构</strong>运转：我们信任银行管理资金，信任科技公司保管数据。这种对中央权威的依赖往往很高效，但也带来了与控制、授权和信任相关的诸多问题。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-中央集权货币的缺陷">1. 中央集权货币的缺陷<a href="https://blog.kuizuo.me/blog/what-is-blockchain#1-%E4%B8%AD%E5%A4%AE%E9%9B%86%E6%9D%83%E8%B4%A7%E5%B8%81%E7%9A%84%E7%BC%BA%E9%99%B7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>在传统金融体系里，汇款不是「直接交易」，而是向<strong>银行等中央中介</strong>发出请求，由它们更新内部记录。你信任银行从你账户扣款，并与其他银行协调，把款项记入收款人账户。<strong>权力高度集中在少数机构手中</strong>。</p>
<ul>
<li><strong>绝对控制</strong>：中央机构可以单方面冻结你的资产、阻止交易或停止服务，你必须获得其许可才能使用自己的资金。</li>
<li><strong>排斥</strong>：系统容易把人排除在外。例如在银行基础设施薄弱的国家，自由职业者可能因没有固定住址（银行开户要求）而无法开户，无法接收国际付款，被中心化的「守门人」挡在全球经济之外。</li>
<li><strong>隐私缺失与审查</strong>：2022 年以太坊创始人 Vitalik Buterin 想向乌克兰人道援助捐款。若走传统银行，会留下公开交易记录，可能引起敌对政府注意、让受助者面临风险。通过区块链上的加密货币，他可以即时转账，无需任何能阻止、监控或审查交易的中央机构。</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-集中式协议的挑战">2. 集中式协议的挑战<a href="https://blog.kuizuo.me/blog/what-is-blockchain#2-%E9%9B%86%E4%B8%AD%E5%BC%8F%E5%8D%8F%E8%AE%AE%E7%9A%84%E6%8C%91%E6%88%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>除了金融，我们还依赖<strong>中央权威</strong>作为集体协议的「权威来源」——这只有在大家都信任该机构公平公正时才成立。</p>
<p>例如：苹果党、梨党、香蕉党进行城镇选举，中央委员会秘密收集并清点纸质选票。当委员会宣布苹果党获胜时，落败方指控舞弊，却<strong>无法独立核实结果</strong>。委员会掌握「真相」，一旦信任动摇，整个制度的合法性就会崩溃。</p>
<p>本质问题在于：<strong>公民无法独立审计过程</strong>，只能信任中间机构；一旦信任受质疑，也没有数学或可验证的方式化解争议。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二解决方案信任最小化系统">二、解决方案：信任最小化系统<a href="https://blog.kuizuo.me/blog/what-is-blockchain#%E4%BA%8C%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%E4%BF%A1%E4%BB%BB%E6%9C%80%E5%B0%8F%E5%8C%96%E7%B3%BB%E7%BB%9F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>区块链为货币和协议提供了一种新范式——<strong>不依赖对某个人或机构的信任</strong>。它构建的是<strong>信任最小化</strong>的系统：参与者依赖的是透明、可验证的<strong>数学模型和代码</strong>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-共享的去中心化的账本">1. 共享的、去中心化的账本<a href="https://blog.kuizuo.me/blog/what-is-blockchain#1-%E5%85%B1%E4%BA%AB%E7%9A%84%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E7%9A%84%E8%B4%A6%E6%9C%AC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>区块链不是每家银行各管一本秘密账本，而是<strong>一本单一的、共享的、分布式的数字账本</strong>。可以把它想象成一本「全球记录簿」，被复制并分布在世界各地成千上万台计算机上。每台持有账本副本的计算机或参与者称为一个<strong>节点</strong>。</p>
<ul>
<li>这些节点组成<strong>去中心化的点对点（P2P）网络</strong>。</li>
<li>没有中央服务器，也没有单点故障。</li>
<li>新交易被发布到网络中，在节点间传播，直到每个参与者都拥有<strong>相同且最新</strong>的账本副本。</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-区块账簿的页">2. 区块：账簿的「页」<a href="https://blog.kuizuo.me/blog/what-is-blockchain#2-%E5%8C%BA%E5%9D%97%E8%B4%A6%E7%B0%BF%E7%9A%84%E9%A1%B5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>交易不是一条条零散写入账本，而是被<strong>收集、打包成一组组</strong>，称为<strong>区块</strong>。可以把区块理解为全球记录簿中的<strong>一页</strong>。当一页填满经过验证的交易后，这一页就被追加到账本中。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-链用密码学串起来的信任">3. 链：用密码学串起来的信任<a href="https://blog.kuizuo.me/blog/what-is-blockchain#3-%E9%93%BE%E7%94%A8%E5%AF%86%E7%A0%81%E5%AD%A6%E4%B8%B2%E8%B5%B7%E6%9D%A5%E7%9A%84%E4%BF%A1%E4%BB%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这就是区块链里**「链」<strong>的由来：区块按时间顺序</strong>链接在一起**，用密码学保护，形成一条<strong>永久、连贯</strong>的历史记录。</p>
<h4 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="哈希值">哈希值<a href="https://blog.kuizuo.me/blog/what-is-blockchain#%E5%93%88%E5%B8%8C%E5%80%BC" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h4>
<ul>
<li>区块生成时，其中所有交易数据经过<strong>加密算法</strong>处理，得到一个<strong>哈希值</strong>（唯一代码）。</li>
<li>哈希值相当于该区块的<strong>数字指纹</strong>或「认证印章」。</li>
<li>交易数据中哪怕只改一个字符，哈希值也会完全不同。</li>
</ul>
<h4 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="区块如何连成链">区块如何「连」成链<a href="https://blog.kuizuo.me/blog/what-is-blockchain#%E5%8C%BA%E5%9D%97%E5%A6%82%E4%BD%95%E8%BF%9E%E6%88%90%E9%93%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h4>
<ul>
<li>每个新区块包含两个关键元素：<strong>自己的哈希值</strong> + <strong>前一个区块的哈希值</strong>。</li>
<li>通过带上前一区块的「指纹」，形成一条<strong>数学链</strong>，从当前区块一直追溯到第一个区块（创世块）。</li>
</ul>
<h4 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="防篡改不可篡改">防篡改（不可篡改）<a href="https://blog.kuizuo.me/blog/what-is-blockchain#%E9%98%B2%E7%AF%A1%E6%94%B9%E4%B8%8D%E5%8F%AF%E7%AF%A1%E6%94%B9" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h4>
<ul>
<li>若有人篡改旧区块中的某笔交易（例如第 10 页），第 10 页的哈希值就会变，「封印」被打破。</li>
<li>第 11 页里记录的是第 10 页的<strong>原始</strong>有效哈希值，链接就会断裂。</li>
<li>这种不一致会沿链传播，其他节点能立刻发现篡改，<strong>自动拒绝</strong>伪造的账本版本，从而维护真实记录的完整性。</li>
</ul>
<h4 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="区块链和-git像在哪不同在哪">区块链和 Git：像在哪、不同在哪<a href="https://blog.kuizuo.me/blog/what-is-blockchain#%E5%8C%BA%E5%9D%97%E9%93%BE%E5%92%8C-git%E5%83%8F%E5%9C%A8%E5%93%AA%E4%B8%8D%E5%90%8C%E5%9C%A8%E5%93%AA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h4>
<p>如果你用过 <strong>Git</strong>，会发现和区块链「长得有点像」：都是<strong>按顺序链接的提交/区块</strong>，每个都带<strong>哈希</strong>，改历史会改掉后面的哈希，一查就能发现。但用途和信任模型不一样：</p>
<table><thead><tr><th>维度</th><th>Git</th><th>区块链</th></tr></thead><tbody><tr><td><strong>结构</strong></td><td>提交（commit）用哈希串成历史链</td><td>区块用哈希串成链，本质相同</td></tr><tr><td><strong>副本</strong></td><td>每人本地一份仓库，可推送/拉取</td><td>每个节点一份账本，通过共识同步</td></tr><tr><td><strong>谁说了算</strong></td><td>没有全球「唯一真相」——你本地可以改历史，推不推、别人认不认是另一回事</td><td>全网对<strong>同一条链</strong>达成共识，没有「我改我的副本」这种选项</td></tr><tr><td><strong>目的</strong></td><td>版本控制、协作写代码，不涉及「谁拥有什么」</td><td>对<strong>状态/资产</strong>达成一致，涉及价值与权限</td></tr><tr><td><strong>篡改</strong></td><td>改历史只影响你本地；推送到远程可能被拒绝或产生分支</td><td>改历史会破坏哈希链，其他节点不认，篡改的链不会被接纳</td></tr></tbody></table>
<p>一句话：Git 是<strong>分布式版本历史</strong>，区块链是<strong>分布式共识账本</strong>——前者管「代码改了什么」，后者管「大家共同承认的当前状态是谁的、发生了什么」。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-数字货币网络的燃料">4. 数字货币：网络的「燃料」<a href="https://blog.kuizuo.me/blog/what-is-blockchain#4-%E6%95%B0%E5%AD%97%E8%B4%A7%E5%B8%81%E7%BD%91%E7%BB%9C%E7%9A%84%E7%87%83%E6%96%99" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>要构建<strong>无需银行</strong>的去中心化金融系统，区块链需要自己的<strong>数字货币（加密货币）</strong>。这些货币只存在于区块链账本上，让用户在新一代、信任最小化的框架内<strong>直接、安全地转移价值</strong>，无需任何中央机构的许可。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三总结">三、总结<a href="https://blog.kuizuo.me/blog/what-is-blockchain#%E4%B8%89%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>说白了就三件事：</p>
<ol>
<li><strong>传统世界的问题</strong>：钱和协议都靠「信人」——信银行、信平台、信委员会；一旦他们不靠谱或作恶，我们没法自己验证，只能认栽。</li>
<li><strong>区块链在干什么</strong>：不靠任何一家中心，大家共用一个账本，用数学和代码保证「谁都不能偷偷改」——从「信人」变成「信规则」。</li>
<li><strong>它长什么样</strong>：账本是一页一页的<strong>区块</strong>，按顺序用<strong>哈希</strong>串成一条<strong>链</strong>；改历史会被立刻发现，所以是<strong>防篡改</strong>的。上面跑的是<strong>数字货币</strong>，用来在没银行的情况下转价值。</li>
</ol>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="区块链" term="区块链"/>
        <category label="Web3" term="Web3"/>
        <category label="去中心化" term="去中心化"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[区块链的历史]]></title>
        <id>https://blog.kuizuo.me/blog/blockchain-history</id>
        <link href="https://blog.kuizuo.me/blog/blockchain-history"/>
        <updated>2026-02-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[从数字现金探索、双花与拜占庭问题，到比特币与以太坊——区块链技术如何诞生并演进为「世界计算机」。]]></summary>
        <content type="html"><![CDATA[<p><strong>从数字现金到比特币，再到以太坊</strong></p>
<p>要理解区块链的威力和用途，得先看它的起源：人类对<strong>真正数字货币</strong>的长期探索。这段历史不是从「区块链」三个字开始，而是从数字世界里<strong>创造和转移价值</strong>所面临的根本挑战开始——一个困扰计算机科学家几十年的问题，最终由比特币第一次在工程上解决。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一比特币第一个去中心化数字货币">一、比特币：第一个去中心化数字货币<a href="https://blog.kuizuo.me/blog/blockchain-history#%E4%B8%80%E6%AF%94%E7%89%B9%E5%B8%81%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E6%95%B0%E5%AD%97%E8%B4%A7%E5%B8%81" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>2008 年</strong>，化名**中本聪（Satoshi Nakamoto）<strong>的人或团体发表论文，首次提出比特币。比特币既是一种数字货币，也是其底层</strong>去中心化网络（区块链）**的名字。它很快被称作「<strong>数字黄金</strong>」：像贵金属一样，<strong>供应量可验证、有限</strong>——代码规定总量上限约 <strong>2100 万枚</strong>，成为一种稀缺的数字资产。</p>
<p>比特币的革命性在于：它是第一个让<strong>世界上任意两个人</strong>在<strong>不依赖银行、卡组织或政府</strong>等可信中介的前提下，完成数字价值的发送与存储的系统。它不是「数字现金」的第一次尝试，却是第一个**真正解决「双重支付问题」**的系统。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二解决双重支付问题">二、解决双重支付问题<a href="https://blog.kuizuo.me/blog/blockchain-history#%E4%BA%8C%E8%A7%A3%E5%86%B3%E5%8F%8C%E9%87%8D%E6%94%AF%E4%BB%98%E9%97%AE%E9%A2%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>双重支付</strong>指的是：同一笔数字货币可能被花两次的风险。实物现金没有这个问题——钞票一旦交给别人，你就不能再花。但数字信息可以复制。</p>
<ul>
<li><strong>类比</strong>：发邮件传照片时，你不会失去原图，只是多了一份副本。若数字货币也这样，你「转」给别人 100 块，对方收到了，你手里还能再花同一笔 100 块——<strong>复制即通胀</strong>，任何数字货币都会失去价值。</li>
<li><strong>早期尝试</strong>：1980 年代的 <strong>DigiCash</strong> 等用<strong>一家中心化公司</strong>维护主账本、验证每笔交易。虽然能防双花，却带来致命缺陷：中心机构可以<strong>冻结资金、拒绝交易、审查用户</strong>，与「摆脱集中控制和单点故障」的目标相悖。</li>
</ul>
<p>所以真正的难题是：<strong>在没有中心的前提下，如何让所有人对「哪笔交易有效、顺序是什么」达成一致？</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三去中心化信任的挑战拜占庭将军问题">三、去中心化信任的挑战：拜占庭将军问题<a href="https://blog.kuizuo.me/blog/blockchain-history#%E4%B8%89%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E4%BF%A1%E4%BB%BB%E7%9A%84%E6%8C%91%E6%88%98%E6%8B%9C%E5%8D%A0%E5%BA%AD%E5%B0%86%E5%86%9B%E9%97%AE%E9%A2%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>在<strong>没有中央领导者</strong>的情况下，让一群参与者对「唯一真相」达成一致，是计算机科学里的经典难题——<strong>拜占庭将军问题</strong>。</p>
<p><strong>比喻</strong>：一群拜占庭将军围城，必须约定同一时间进攻。他们只能靠信使传话，但有的将军或信使可能是叛徒、会传假消息。忠诚的将军要怎样<strong>达成共识</strong>，保证一次统一、成功的进攻？</p>
<p>这和<strong>去中心化节点网络维护货币账本</strong>是一类问题：即使部分节点作恶（例如尝试双花、散布假交易），网络也必须对「哪些交易有效、顺序如何」达成一致。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四中本聪的突破比特币与白皮书">四、中本聪的突破：比特币与白皮书<a href="https://blog.kuizuo.me/blog/blockchain-history#%E5%9B%9B%E4%B8%AD%E6%9C%AC%E8%81%AA%E7%9A%84%E7%AA%81%E7%A0%B4%E6%AF%94%E7%89%B9%E5%B8%81%E4%B8%8E%E7%99%BD%E7%9A%AE%E4%B9%A6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>中本聪在 <strong>2008 年白皮书</strong>里给出的方案，同时应对了<strong>双花</strong>和<strong>拜占庭将军问题</strong>：把<strong>交易账本分发给全网参与者</strong>，去掉对中央权威的依赖。</p>
<ul>
<li>在<strong>点对点</strong>系统中，每个参与者都有一份<strong>完整交易历史</strong>。</li>
<li>新交易由<strong>网络共同验证</strong>，被打包成「<strong>区块</strong>」，追加到已有「<strong>链</strong>」上，形成<strong>永久、不可篡改</strong>的记录。</li>
</ul>
<p>这种设计带来<strong>可信的中立性</strong>：</p>
<table><thead><tr><th>维度</th><th>含义</th></tr></thead><tbody><tr><td><strong>中立</strong></td><td>网络按规则办事，不关心用户身份、地点、财富或目的，只处理符合规则的交易。</td></tr><tr><td><strong>可信</strong></td><td>不依赖某个人或机构的「善意」，规则写在<strong>开源代码</strong>里，由<strong>去中心化网络</strong>共同执行。</td></tr></tbody></table>
<p>结果是：一个<strong>全球、无需许可、抗审查</strong>的货币层，有网络就能用——对货币不稳定、金融封闭的地区，以及被传统银行排除在外的人，意义重大。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五以太坊从数字黄金到世界计算机">五、以太坊：从「数字黄金」到「世界计算机」<a href="https://blog.kuizuo.me/blog/blockchain-history#%E4%BA%94%E4%BB%A5%E5%A4%AA%E5%9D%8A%E4%BB%8E%E6%95%B0%E5%AD%97%E9%BB%84%E9%87%91%E5%88%B0%E4%B8%96%E7%95%8C%E8%AE%A1%E7%AE%97%E6%9C%BA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>比特币问世几年后，<strong>Vitalik Buterin（维塔利克·布特林）<strong>等人认为：<strong>可信中立</strong>不仅可以用于货币，还可以用于</strong>任意协议</strong>。</p>
<p><strong>2015 年</strong>，以太坊上线。和比特币一样，它有原生数字货币 <strong>ETH（以太币）</strong> 驱动网络；不同之处在于：<strong>以太坊能在链上运行代码</strong>，从而执行<strong>智能合约</strong>——即「写进代码、由链自动执行」的协议。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="智能合约代码即不可违背的规则">智能合约：代码即不可违背的规则<a href="https://blog.kuizuo.me/blog/blockchain-history#%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E4%BB%A3%E7%A0%81%E5%8D%B3%E4%B8%8D%E5%8F%AF%E8%BF%9D%E8%83%8C%E7%9A%84%E8%A7%84%E5%88%99" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>概念最早由密码学家 <strong>Nick Szabo（尼克·萨博）</strong> 在 <strong>1994 年</strong> 提出，但直到区块链出现才真正落地。<strong>智能合约</strong>把条款直接写进代码，部署在链上，由网络自动执行、不可单方篡改。</p>
<p><strong>对比</strong>：</p>
<ul>
<li><strong>传统协议</strong>（如购房）：依赖银行、律师、产权机构等多方，耗时长、成本高，且要信任每一方守约。</li>
<li><strong>智能合约</strong>（如航班延误险）：条款写成「若延误超过 24 小时，自动退票款」。合约接入可靠航班数据；条件满足时，代码自动执行、退款到钱包。无需申请理赔、无需和保险公司扯皮，保险公司也无法改条款或拒赔——<strong>逻辑由代码固定</strong>。</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="比特币-vs-以太坊计算器-vs-电脑">比特币 vs 以太坊：计算器 vs 电脑<a href="https://blog.kuizuo.me/blog/blockchain-history#%E6%AF%94%E7%89%B9%E5%B8%81-vs-%E4%BB%A5%E5%A4%AA%E5%9D%8A%E8%AE%A1%E7%AE%97%E5%99%A8-vs-%E7%94%B5%E8%84%91" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<table><thead><tr><th></th><th>比特币</th><th>以太坊</th></tr></thead><tbody><tr><td><strong>定位</strong></td><td>专注一件事：<strong>安全、去中心化的价值存储与转账（去中心化的点对点支付系统）</strong></td><td><strong>可编程平台</strong>：在链上跑任意应用与复杂协议</td></tr><tr><td><strong>设计</strong></td><td>刻意简单、功能受限（<strong>图灵不完备</strong>），优先安全与稳定</td><td><strong>图灵完备</strong>，可编写复杂逻辑，支持智能合约与 DApp</td></tr><tr><td><strong>类比</strong></td><td>像<strong>计算器</strong>——把一件事做到极致</td><td>像<strong>世界计算机</strong>——在同一个可信底座上跑各种程序</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六总结">六、总结<a href="https://blog.kuizuo.me/blog/blockchain-history#%E5%85%AD%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>说白了就四条线：</p>
<ol>
<li><strong>问题从哪来</strong>：人类想要「数字现金」，但数字可复制 → <strong>双花</strong>难解；早期用中心记账（如 DigiCash）能防双花，却重新引入中心化；在无中心下达成一致，又对应<strong>拜占庭将军问题</strong>。</li>
<li><strong>比特币做了什么</strong>：中本聪用<strong>分布式账本 + 区块 + 链 + 共识</strong>，第一次在无中心前提下解决双花和拜占庭问题，建成<strong>可信中立</strong>的全球货币层，被称为「数字黄金」。</li>
<li><strong>以太坊延伸了什么</strong>：把「可信中立」从<strong>货币</strong>扩展到<strong>任意协议</strong>，用<strong>智能合约</strong>把条款写进代码、由链自动执行，区块链从「计算器」变成「世界计算机」。</li>
<li><strong>BTC vs ETH</strong>：比特币 = 极致简单、图灵不完备、价值存储与转账；以太坊 = 图灵完备、可编程、智能合约与 DApp 平台。</li>
</ol>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="区块链" term="区块链"/>
        <category label="比特币" term="比特币"/>
        <category label="以太坊" term="以太坊"/>
        <category label="Web3" term="Web3"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Nestjs快速上手系列一]]></title>
        <id>https://blog.kuizuo.me/blog/learn-nest-1</id>
        <link href="https://blog.kuizuo.me/blog/learn-nest-1"/>
        <updated>2026-02-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[NestJS 快速上手：CLI 常用命令、IoC/DI 思想与 TypeScript 装饰器入门。]]></summary>
        <content type="html"><![CDATA[<p>本系列介绍 NestJS 入门必备：CLI 常用命令、IoC/DI 思想，以及 TypeScript 装饰器在 Nest 中的用法。</p>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="-概述">📝 概述<a href="https://blog.kuizuo.me/blog/learn-nest-1#-%E6%A6%82%E8%BF%B0" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>本文包含三部分：<strong>Nest CLI 常用命令</strong>（新建项目、生成模块/控制器/服务等）、<strong>IoC 与 DI</strong>（控制反转与依赖注入的概念与代码示例）、<strong>装饰器</strong>（方法/类/属性装饰器及在 Nest 中的对应关系）。掌握这些后，可以更顺畅地阅读和编写 Nest 代码。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一nest-cli-常用命令">一、Nest CLI 常用命令<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E4%B8%80nest-cli-%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="项目相关">项目相关<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E9%A1%B9%E7%9B%AE%E7%9B%B8%E5%85%B3" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<table><thead><tr><th>命令</th><th>说明</th></tr></thead><tbody><tr><td><code>nest new 项目名</code></td><td>新建项目</td></tr><tr><td><code>nest start</code></td><td>启动（开发模式，默认）</td></tr><tr><td><code>nest start --watch</code></td><td>监听文件变化并自动重启</td></tr><tr><td><code>nest build</code></td><td>构建生产包</td></tr></tbody></table>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="生成代码最常用">生成代码（最常用）<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E7%94%9F%E6%88%90%E4%BB%A3%E7%A0%81%E6%9C%80%E5%B8%B8%E7%94%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<table><thead><tr><th>命令</th><th>说明</th></tr></thead><tbody><tr><td><code>nest g resource 名称</code></td><td>生成完整 CRUD 模块（controller + service + module + dto）</td></tr><tr><td><code>nest g module 名称</code></td><td>生成模块</td></tr><tr><td><code>nest g controller 名称</code></td><td>生成控制器</td></tr><tr><td><code>nest g service 名称</code></td><td>生成服务</td></tr></tbody></table>
<p>示例：</p>
<div class="language-bash codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-bash codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">nest g resource user          </span><span class="token comment" style="color:hsl(230, 4%, 64%)"># 生成 user 模块，会问是否要 CRUD</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">nest g resource post --no-spec </span><span class="token comment" style="color:hsl(230, 4%, 64%)"># 不生成测试文件</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">nest g module auth</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">nest g controller auth</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">nest g </span><span class="token function" style="color:hsl(221, 87%, 60%)">service</span><span class="token plain"> auth</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="其他">其他<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E5%85%B6%E4%BB%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<table><thead><tr><th>命令</th><th>说明</th></tr></thead><tbody><tr><td><code>nest g guard 名称</code></td><td>守卫</td></tr><tr><td><code>nest g pipe 名称</code></td><td>管道</td></tr><tr><td><code>nest g middleware 名称</code></td><td>中间件</td></tr><tr><td><code>nest info</code></td><td>查看 Nest 与依赖版本</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二ioc控制反转与-di依赖注入">二、IoC（控制反转）与 DI（依赖注入）<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E4%BA%8Cioc%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC%E4%B8%8E-di%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>IoC 与 DI 本质是一体两面：<strong>IoC 是一种思想</strong>（控制权交给容器），<strong>DI 是 IoC 的一种具体实现</strong>（通过注入依赖）。</p>
<p>一句话：<strong>DI 的作用是把依赖的创建和保管交给「容器」统一维护，从而让类与依赖解耦。</strong></p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="没有-di自己创建依赖紧耦合">没有 DI：自己创建依赖（紧耦合）<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E6%B2%A1%E6%9C%89-di%E8%87%AA%E5%B7%B1%E5%88%9B%E5%BB%BA%E4%BE%9D%E8%B5%96%E7%B4%A7%E8%80%A6%E5%90%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-typescript codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-typescript codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token comment" style="color:hsl(230, 4%, 64%)">// B 自己 new 了 A 和 C → 强依赖具体实现，难测试、难替换</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">class</span><span class="token plain"> </span><span class="token class-name constant" style="color:hsl(35, 99%, 36%)">A</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  name</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token function" style="color:hsl(221, 87%, 60%)">constructor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">name</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">name </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> name</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">class</span><span class="token plain"> </span><span class="token class-name constant" style="color:hsl(35, 99%, 36%)">C</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  name</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token function" style="color:hsl(221, 87%, 60%)">constructor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">name</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">name </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> name</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">class</span><span class="token plain"> </span><span class="token class-name" style="color:hsl(35, 99%, 36%)">B_WithoutDI</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  a</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token constant" style="color:hsl(35, 99%, 36%)">A</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  c</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token constant" style="color:hsl(35, 99%, 36%)">C</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token function" style="color:hsl(221, 87%, 60%)">constructor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">a </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">new</span><span class="token plain"> </span><span class="token class-name constant" style="color:hsl(35, 99%, 36%)">A</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'1'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"> </span><span class="token comment" style="color:hsl(230, 4%, 64%)">// 控制权在 B 内部</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">c </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">new</span><span class="token plain"> </span><span class="token class-name constant" style="color:hsl(35, 99%, 36%)">C</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'2'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="有-di依赖由外部注入解耦">有 DI：依赖由外部注入（解耦）<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E6%9C%89-di%E4%BE%9D%E8%B5%96%E7%94%B1%E5%A4%96%E9%83%A8%E6%B3%A8%E5%85%A5%E8%A7%A3%E8%80%A6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-typescript codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-typescript codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token comment" style="color:hsl(230, 4%, 64%)">// 「容器」负责创建和保管实例，需要时再取出来 → 控制权在容器 = IoC</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">class</span><span class="token plain"> </span><span class="token class-name" style="color:hsl(35, 99%, 36%)">Container</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  modules</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> Record</span><span class="token operator" style="color:hsl(221, 87%, 60%)">&lt;</span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token operator" style="color:hsl(221, 87%, 60%)">&gt;</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token function" style="color:hsl(221, 87%, 60%)">constructor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">modules </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token function" style="color:hsl(221, 87%, 60%)">provide</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">key</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> instance</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">modules</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">[</span><span class="token plain">key</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">]</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> instance</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token generic-function function" style="color:hsl(221, 87%, 60%)">get</span><span class="token generic-function generic class-name operator" style="color:hsl(221, 87%, 60%)">&lt;</span><span class="token generic-function generic class-name constant" style="color:hsl(35, 99%, 36%)">T</span><span class="token generic-function generic class-name" style="color:hsl(35, 99%, 36%)"> </span><span class="token generic-function generic class-name operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token generic-function generic class-name" style="color:hsl(35, 99%, 36%)"> </span><span class="token generic-function generic class-name builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token generic-function generic class-name operator" style="color:hsl(221, 87%, 60%)">&gt;</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">key</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token constant" style="color:hsl(35, 99%, 36%)">T</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">return</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">modules</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">[</span><span class="token plain">key</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">]</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">as</span><span class="token plain"> </span><span class="token constant" style="color:hsl(35, 99%, 36%)">T</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token comment" style="color:hsl(230, 4%, 64%)">// B 不再 new A/C，只声明「我需要 a 和 c」，由容器注入 → 这就是 DI</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">class</span><span class="token plain"> </span><span class="token class-name" style="color:hsl(35, 99%, 36%)">B_WithDI</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  a</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token constant" style="color:hsl(35, 99%, 36%)">A</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  c</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token constant" style="color:hsl(35, 99%, 36%)">C</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token function" style="color:hsl(221, 87%, 60%)">constructor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">container</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> Container</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">a </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> container</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token generic-function function" style="color:hsl(221, 87%, 60%)">get</span><span class="token generic-function generic class-name operator" style="color:hsl(221, 87%, 60%)">&lt;</span><span class="token generic-function generic class-name constant" style="color:hsl(35, 99%, 36%)">A</span><span class="token generic-function generic class-name operator" style="color:hsl(221, 87%, 60%)">&gt;</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'a'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"> </span><span class="token comment" style="color:hsl(230, 4%, 64%)">// 依赖从外部注入</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">c </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> container</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token generic-function function" style="color:hsl(221, 87%, 60%)">get</span><span class="token generic-function generic class-name operator" style="color:hsl(221, 87%, 60%)">&lt;</span><span class="token generic-function generic class-name constant" style="color:hsl(35, 99%, 36%)">C</span><span class="token generic-function generic class-name operator" style="color:hsl(221, 87%, 60%)">&gt;</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'c'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p><strong>小结：</strong></p>
<ul>
<li><strong>DI 的好处</strong>：解耦、易测、易扩展、职责更清晰。</li>
<li><strong>前端类比</strong>：不自己造依赖、也不靠一层层 props 传，而是像 Context / provide-inject 那样「声明需要什么，由外层/容器提供」；Nest 的 DI 是同一思想在后端：容器负责创建和注入，你只在构造函数里「声明要什么」。</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三装饰器">三、装饰器<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E4%B8%89%E8%A3%85%E9%A5%B0%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>装饰器（Decorator）</strong> 是一个函数，用来「标注」类、方法、属性或参数，在<strong>类定义时</strong>执行，用于改变类或成员的行为。在 TypeScript 中有四种：<strong>类装饰器、方法装饰器、属性装饰器、参数装饰器</strong>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="方法装饰器methoddecorator">方法装饰器（MethodDecorator）<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E6%96%B9%E6%B3%95%E8%A3%85%E9%A5%B0%E5%99%A8methoddecorator" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>签名：<code>(target, key, descriptor) =&gt; void | PropertyDescriptor</code></p>
<ul>
<li><code>target</code>：类的原型（实例方法）或构造函数（静态方法）</li>
<li><code>key</code>：方法名（string | symbol）</li>
<li><code>descriptor</code>：该方法的属性描述符（value、writable、enumerable 等）</li>
</ul>
<div class="language-typescript codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-typescript codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token keyword" style="color:hsl(301, 63%, 40%)">const</span><span class="token plain"> currency</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token function-variable function" style="color:hsl(221, 87%, 60%)">MethodDecorator</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  target</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> object</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  key</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">|</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">symbol</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  descriptor</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> PropertyDescriptor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">console</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token function" style="color:hsl(221, 87%, 60%)">log</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'target（原型）:'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> target</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">console</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token function" style="color:hsl(221, 87%, 60%)">log</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'key（方法名）:'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> key</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">console</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token function" style="color:hsl(221, 87%, 60%)">log</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'descriptor（描述符）:'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> descriptor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">return</span><span class="token plain"> descriptor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">class</span><span class="token plain"> </span><span class="token class-name" style="color:hsl(35, 99%, 36%)">Dog</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">public</span><span class="token plain"> name</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token function" style="color:hsl(221, 87%, 60%)">constructor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">name </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> </span><span class="token string" style="color:hsl(119, 34%, 47%)">''</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token decorator at operator" style="color:hsl(221, 87%, 60%)">@</span><span class="token decorator function" style="color:hsl(221, 87%, 60%)">currency</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token function" style="color:hsl(221, 87%, 60%)">getName</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">name</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> age</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">number</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">return</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:hsl(119, 34%, 47%)">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">${</span><span class="token template-string interpolation keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token template-string interpolation punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token template-string interpolation">name</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token template-string string" style="color:hsl(119, 34%, 47%)">-</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">${</span><span class="token template-string interpolation">name</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token template-string string" style="color:hsl(119, 34%, 47%)">-</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">${</span><span class="token template-string interpolation">age</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token template-string template-punctuation string" style="color:hsl(119, 34%, 47%)">`</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">const</span><span class="token plain"> x </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">new</span><span class="token plain"> </span><span class="token class-name" style="color:hsl(35, 99%, 36%)">Dog</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">x</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token function" style="color:hsl(221, 87%, 60%)">getName</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'a'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> </span><span class="token number" style="color:hsl(35, 99%, 36%)">1</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="方法装饰器常见用法包装原方法">方法装饰器常见用法：包装原方法<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E6%96%B9%E6%B3%95%E8%A3%85%E9%A5%B0%E5%99%A8%E5%B8%B8%E8%A7%81%E7%94%A8%E6%B3%95%E5%8C%85%E8%A3%85%E5%8E%9F%E6%96%B9%E6%B3%95" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<div class="language-typescript codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-typescript codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token keyword" style="color:hsl(301, 63%, 40%)">function</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">log</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  _target</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> object</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  key</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">|</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">symbol</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  descriptor</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> PropertyDescriptor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">void</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">const</span><span class="token plain"> original </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> descriptor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">value </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">as</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token operator" style="color:hsl(221, 87%, 60%)">...</span><span class="token plain">args</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">[</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">]</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=&gt;</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  descriptor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token function-variable function" style="color:hsl(221, 87%, 60%)">value</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">function</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">...</span><span class="token plain">args</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">[</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">]</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">console</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token function" style="color:hsl(221, 87%, 60%)">log</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token template-string template-punctuation string" style="color:hsl(119, 34%, 47%)">`</span><span class="token template-string string" style="color:hsl(119, 34%, 47%)">调用 </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">${</span><span class="token template-string interpolation function" style="color:hsl(221, 87%, 60%)">String</span><span class="token template-string interpolation punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token template-string interpolation">key</span><span class="token template-string interpolation punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token template-string template-punctuation string" style="color:hsl(119, 34%, 47%)">`</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> args</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">return</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">original</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token function" style="color:hsl(221, 87%, 60%)">apply</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token keyword" style="color:hsl(301, 63%, 40%)">this</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> args</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">as</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">class</span><span class="token plain"> </span><span class="token class-name" style="color:hsl(35, 99%, 36%)">Service</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token decorator at operator" style="color:hsl(221, 87%, 60%)">@</span><span class="token decorator function" style="color:hsl(221, 87%, 60%)">log</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token function" style="color:hsl(221, 87%, 60%)">greet</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">msg</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">return</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:hsl(119, 34%, 47%)">`</span><span class="token template-string string" style="color:hsl(119, 34%, 47%)">Hello, </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">${</span><span class="token template-string interpolation">msg</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token template-string template-punctuation string" style="color:hsl(119, 34%, 47%)">`</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">const</span><span class="token plain"> s </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">new</span><span class="token plain"> </span><span class="token class-name" style="color:hsl(35, 99%, 36%)">Service</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">s</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token function" style="color:hsl(221, 87%, 60%)">greet</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'Nest'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"> </span><span class="token comment" style="color:hsl(230, 4%, 64%)">// 控制台: 调用 greet ['Nest']，返回值: 'Hello, Nest'</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="类装饰器classdecorator">类装饰器（ClassDecorator）<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E7%B1%BB%E8%A3%85%E9%A5%B0%E5%99%A8classdecorator" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>参数只有一个：<code>target</code> 是构造函数本身。</p>
<div class="language-typescript codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-typescript codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token keyword" style="color:hsl(301, 63%, 40%)">function</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">Component</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">constructor</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">new</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token operator" style="color:hsl(221, 87%, 60%)">...</span><span class="token plain">args</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">unknown</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">[</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">]</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=&gt;</span><span class="token plain"> object</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">console</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token function" style="color:hsl(221, 87%, 60%)">log</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'类装饰器:'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> constructor</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">.</span><span class="token plain">name</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token decorator at operator" style="color:hsl(221, 87%, 60%)">@</span><span class="token decorator function" style="color:hsl(221, 87%, 60%)">Component</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">class</span><span class="token plain"> </span><span class="token class-name" style="color:hsl(35, 99%, 36%)">App</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="属性装饰器propertydecorator">属性装饰器（PropertyDecorator）<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E5%B1%9E%E6%80%A7%E8%A3%85%E9%A5%B0%E5%99%A8propertydecorator" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>参数：<code>(target 原型, key 属性名)</code>，没有 descriptor（需配合 reflect-metadata 等做更多事）。</p>
<div class="language-typescript codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-typescript codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token keyword" style="color:hsl(301, 63%, 40%)">function</span><span class="token plain"> </span><span class="token function" style="color:hsl(221, 87%, 60%)">Default</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">value</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">target</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> object</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> key</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">|</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">symbol</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token plain">target </span><span class="token keyword" style="color:hsl(301, 63%, 40%)">as</span><span class="token plain"> Record</span><span class="token operator" style="color:hsl(221, 87%, 60%)">&lt;</span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">|</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">symbol</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">,</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token operator" style="color:hsl(221, 87%, 60%)">&gt;</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">[</span><span class="token plain">key</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">]</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">=</span><span class="token plain"> value</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token keyword" style="color:hsl(301, 63%, 40%)">class</span><span class="token plain"> </span><span class="token class-name" style="color:hsl(35, 99%, 36%)">Config</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  </span><span class="token decorator at operator" style="color:hsl(221, 87%, 60%)">@</span><span class="token decorator function" style="color:hsl(221, 87%, 60%)">Default</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token string" style="color:hsl(119, 34%, 47%)">'dev'</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">  env</span><span class="token operator" style="color:hsl(221, 87%, 60%)">!</span><span class="token operator" style="color:hsl(221, 87%, 60%)">:</span><span class="token plain"> </span><span class="token builtin" style="color:hsl(119, 34%, 47%)">string</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">}</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="和-nest-的对应关系">和 Nest 的对应关系<a href="https://blog.kuizuo.me/blog/learn-nest-1#%E5%92%8C-nest-%E7%9A%84%E5%AF%B9%E5%BA%94%E5%85%B3%E7%B3%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>Nest 里大量用装饰器做「声明式」配置，例如：</p>
<table><thead><tr><th>装饰器</th><th>类型</th><th>作用</th></tr></thead><tbody><tr><td><code>@Controller('user')</code></td><td>类装饰器</td><td>标记为控制器并指定路由前缀</td></tr><tr><td><code>@Get()</code></td><td>方法装饰器</td><td>标记为 GET 且绑定到对应路径</td></tr><tr><td><code>@Injectable()</code></td><td>类装饰器</td><td>标记可被 DI 注入</td></tr><tr><td><code>@Body()</code> / <code>@Query()</code></td><td>参数装饰器</td><td>标记参数从哪来（body/query）</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="-总结">🎯 总结<a href="https://blog.kuizuo.me/blog/learn-nest-1#-%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<ul>
<li><strong>Nest CLI</strong>：<code>nest g resource/module/controller/service</code> 等可快速生成骨架代码。</li>
<li><strong>IoC/DI</strong>：把依赖的创建与保管交给容器，类只声明「需要什么」，实现解耦与易测。</li>
<li><strong>装饰器</strong>：TS 的类/方法/属性/参数装饰器在 Nest 中对应 <code>@Controller</code>、<code>@Get</code>、<code>@Injectable</code>、<code>@Body</code> 等，用于声明式配置。</li>
</ul>
<p>下一期可以继续写 Nest 的模块、控制器与服务的实际用法。</p>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="NestJS" term="NestJS"/>
        <category label="TypeScript" term="TypeScript"/>
        <category label="Node.js" term="Node.js"/>
        <category label="后端" term="后端"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[区块链的优势]]></title>
        <id>https://blog.kuizuo.me/blog/blockchain-advantages</id>
        <link href="https://blog.kuizuo.me/blog/blockchain-advantages"/>
        <updated>2026-02-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[区块链的四大优势：无需许可的所有权、可信中立、去中心化韧性、不可篡改与透明——为何说它把权力和自主权还给个人。]]></summary>
        <content type="html"><![CDATA[<p><strong>从「要许可」到「无需许可」，权力和自主权回到你手上</strong></p>
<p>说人话就是：区块链为啥被说得那么神？先想一件事——<strong>钱到底算不算你的</strong>。传统世界里，你想用钱，得看银行、平台给不给你许可；他们说不算就不算。区块链把这事儿反过来了：<strong>不用谁批准</strong>，链上的资产就是你自己的，你说了算。那**区块链是怎么做到这一点的？**下面从四个方向说清楚。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一无需许可谁才真正拥有你的钱">一、无需许可：谁才真正「拥有」你的钱？<a href="https://blog.kuizuo.me/blog/blockchain-advantages#%E4%B8%80%E6%97%A0%E9%9C%80%E8%AE%B8%E5%8F%AF%E8%B0%81%E6%89%8D%E7%9C%9F%E6%AD%A3%E6%8B%A5%E6%9C%89%E4%BD%A0%E7%9A%84%E9%92%B1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>在传统体系里，<strong>使用权 = 被授权</strong>。开户、跨境汇款、大额取现或消费，常常需要银行等机构的许可；它们可以随时调整、限制或收回这种许可。于是有一个根本问题：<strong>如果别人能剥夺你使用自己资金的权利，这笔钱还算不算是「你的」？</strong></p>
<p>区块链改变了这一点：在链上，<strong>你对资产拥有真正的所有权和控制权</strong>。</p>
<ul>
<li>没有中央机构或中介能<strong>拒绝你接入网络</strong>或<strong>阻止你发起交易</strong>。</li>
<li>你<strong>不需要向任何人申请许可</strong>就能使用属于自己的资产。</li>
</ul>
<p>从「许可制」到「<strong>无需许可（Permissionless）</strong>」，权力和自主权回到了个人手里。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二可信的中立性不偏袒任何一方的体系">二、可信的中立性：不偏袒任何一方的体系<a href="https://blog.kuizuo.me/blog/blockchain-advantages#%E4%BA%8C%E5%8F%AF%E4%BF%A1%E7%9A%84%E4%B8%AD%E7%AB%8B%E6%80%A7%E4%B8%8D%E5%81%8F%E8%A2%92%E4%BB%BB%E4%BD%95%E4%B8%80%E6%96%B9%E7%9A%84%E4%BD%93%E7%B3%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>传统金融并不总是中立的，歧视在实践中很常见：因为国籍、信用分数甚至政治立场，就可能被拒之门外。很多服务要求政府身份证或实体地址，直接把全球数以百万计的人排除在外；银行也可以对某些国家或地区关闭国际支付，相当于把一整国人与全球经济隔开。</p>
<p>相比之下，区块链追求<strong>可信的中立性</strong>：</p>
<ul>
<li><strong>底层协议</strong>（运行网络的代码）被设计成<strong>只按规则办事</strong>，不关心你的姓名、所在地、财富或背景。</li>
<li>系统<strong>无法在规则层面偏袒谁</strong>，因为执行的是代码，不是人的主观判断。</li>
<li>交易是否有效，由<strong>数学与规则</strong>判定，而不是人为偏见。</li>
</ul>
<p>结果是：<strong>规则对所有人一视同仁</strong>，谁都可以在同等条件下使用网络，形成更公平的竞争环境。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三去中心化没有单点难以被关掉">三、去中心化：没有单点，难以被「关掉」<a href="https://blog.kuizuo.me/blog/blockchain-advantages#%E4%B8%89%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E6%B2%A1%E6%9C%89%E5%8D%95%E7%82%B9%E9%9A%BE%E4%BB%A5%E8%A2%AB%E5%85%B3%E6%8E%89" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这些优势的底层支撑是<strong>去中心化</strong>。</p>
<p><strong>传统系统</strong>：中心化 → 存在<strong>单一控制点</strong>，也就存在<strong>单一故障点</strong>。政府可以关停银行，银行可以冻结你的账户，公司可以倒闭、卷走客户资金；中央服务器被查封或宕机，整个服务就可能瘫痪。</p>
<p><strong>区块链</strong>：<strong>去中心化</strong>。</p>
<ul>
<li>不是由一家公司或一台服务器运行，而是由全球**成千上万台计算机（节点）**共同维护。</li>
<li>每个节点都存有一份<strong>相同的数据副本</strong>，没有「总开关」可被攻击或关停。</li>
<li>要阻止这个网络，相当于要关掉整个互联网；即便某些国家或机构试图封锁，没有单一实体能控制全网，<strong>网络仍可继续运行、处理交易</strong>。</li>
</ul>
<p>因此具备很强的<strong>抗审韧性、安全性和不可阻挡性</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四不可篡改与透明用代码构建信任">四、不可篡改与透明：用代码构建信任<a href="https://blog.kuizuo.me/blog/blockchain-advantages#%E5%9B%9B%E4%B8%8D%E5%8F%AF%E7%AF%A1%E6%94%B9%E4%B8%8E%E9%80%8F%E6%98%8E%E7%94%A8%E4%BB%A3%E7%A0%81%E6%9E%84%E5%BB%BA%E4%BF%A1%E4%BB%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>传统金融与法律体系往往<strong>不透明</strong>：关键决策在幕后做出，规则也可能随时改变且事先不通知——银行改费率、公司在用户不知情时改条款、政府一夜之间修法，都会带来不确定和信任缺失。</p>
<p>区块链从两个维度回应：</p>
<table><thead><tr><th>维度</th><th>含义</th></tr></thead><tbody><tr><td><strong>透明</strong></td><td>公共链上的交易对所有人<strong>可查</strong>；任何人都可以<strong>审计</strong>网络、验证资金流向、确认系统是否按规则运行；协议规则以<strong>开源代码</strong>形式存在，人人可读。</td></tr><tr><td><strong>不可篡改</strong></td><td>交易一旦被验证并写入链上，就<strong>无法被修改、篡改或删除</strong>，形成<strong>永久、不可更改</strong>的历史记录。</td></tr></tbody></table>
<p>两者一起，让系统的行为<strong>可预测、可验证</strong>：你不需要相信某个人或某家公司「会守信」，只需要相信<strong>代码会按公开写好的规则执行</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五总结">五、总结<a href="https://blog.kuizuo.me/blog/blockchain-advantages#%E4%BA%94%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>说白了就四点：</p>
<ol>
<li><strong>无需许可</strong>：链上你对资产有真正所有权和控制权，不需要银行或平台「批准」才能用——从「被授权使用」变成「本来就属于你」。</li>
<li><strong>可信中立</strong>：协议按规则运行，不认人、不认身份，只认是否符合规则；验证靠数学，不靠人为偏好，规则对所有人一致。</li>
<li><strong>去中心化</strong>：没有单点控制、没有单点故障，关不掉、审不死，网络由全球节点共同维护，抗审、安全、难以阻挡。</li>
<li><strong>不可篡改 + 透明</strong>：链上记录可查、可审计，且写上去就改不了；信任来自代码和规则，而不是来自某家机构的口头承诺。</li>
</ol>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="区块链" term="区块链"/>
        <category label="Web3" term="Web3"/>
        <category label="去中心化" term="去中心化"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[区块链的应用]]></title>
        <id>https://blog.kuizuo.me/blog/blockchain-applications</id>
        <link href="https://blog.kuizuo.me/blog/blockchain-applications"/>
        <updated>2026-02-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[区块链在现实里到底在干啥？从数字货币、DeFi、跨境支付、数字所有权到 DAO，外加常见质疑的回应。]]></summary>
        <content type="html"><![CDATA[<p><strong>抛开炒作，链上到底在解决啥实际问题？</strong></p>
<p>说人话就是：区块链不只有「炒币」——它已经在干很多具体事了：<strong>转钱不用银行、协议自动执行、数字东西真能算你的、一群人一起投票管钱</strong>……那**这些事具体是怎么落地、用在哪些场景的？**下面按「钱 → 协议 → 所有权 → 治理 → 术语与质疑」捋一遍。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一数字货币与-defi">一、数字货币与 DeFi<a href="https://blog.kuizuo.me/blog/blockchain-applications#%E4%B8%80%E6%95%B0%E5%AD%97%E8%B4%A7%E5%B8%81%E4%B8%8E-defi" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>区块链最直观的应用就是<strong>数字货币（加密货币）</strong>：两个人之间可以直接在互联网上转价值，<strong>不经过银行</strong>。比特币（BTC）、以太币（ETH）就是典型——一个跑在比特币链上，一个跑在以太坊链上。</p>
<p>再往外扩一层是 <strong>DeFi（去中心化金融）</strong>，其中很重要的一块是<strong>稳定币</strong>：</p>
<ul>
<li><strong>稳定币</strong>：和法币（如美元）挂钩的数字货币，价值尽量稳定。例如 <strong>USDC</strong>，1 USDC ≈ 1 美元。</li>
<li><strong>为啥有用</strong>：在通胀严重的国家，人们可以用稳定币存钱、结算，避免本币暴跌把储蓄蒸发掉。</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二智能合约协议写进代码自动执行">二、智能合约：协议写进代码、自动执行<a href="https://blog.kuizuo.me/blog/blockchain-applications#%E4%BA%8C%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%8D%8F%E8%AE%AE%E5%86%99%E8%BF%9B%E4%BB%A3%E7%A0%81%E8%87%AA%E5%8A%A8%E6%89%A7%E8%A1%8C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>智能合约</strong> = 把条款写成代码、部署在链上，条件满足就<strong>自动执行</strong>，不用再找中间人审批。可以理解成「合同 + 自动执行器」，所以<strong>透明、不可篡改、省人工</strong>。</p>
<p><strong>例子——自动化保险</strong>：保单写成智能合约，接入可信数据源（如天气、航班）。一旦触发条件（如「某地发生重大洪水」），合约自动给投保人打款，不用填表、不用等保险公司审批，理赔更快、规则写死不能赖账。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三跨境支付">三、跨境支付<a href="https://blog.kuizuo.me/blog/blockchain-applications#%E4%B8%89%E8%B7%A8%E5%A2%83%E6%94%AF%E4%BB%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>传统跨境汇款：慢（几天）、贵（手续费高）、很多人用不上（PayPal/Venmo 不是全球都能用）。</p>
<p>链上<strong>几分钟到账、成本低</strong>，有网就能用。很多应用已经把「给国外亲友汇款」做成接近「发一条消息」的体验，不依赖银行电汇。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四可验证的数字所有权">四、可验证的数字所有权<a href="https://blog.kuizuo.me/blog/blockchain-applications#%E5%9B%9B%E5%8F%AF%E9%AA%8C%E8%AF%81%E7%9A%84%E6%95%B0%E5%AD%97%E6%89%80%E6%9C%89%E6%9D%83" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>区块链能把「谁拥有什么」记在<strong>不可篡改的账本</strong>上，变成<strong>可验证、难伪造</strong>的「所有权证明」。典型形式包括 <strong>NFT</strong> 等<strong>唯一代币</strong>。</p>
<table><thead><tr><th>场景</th><th>怎么用</th></tr></thead><tbody><tr><td><strong>数字艺术 / 音乐</strong></td><td>创作者直接卖给粉丝，少被画廊、唱片公司抽成；链上可查归属与流转。</td></tr><tr><td><strong>游戏内物品</strong></td><td>装备、皮肤等真正「属于」玩家，可在开放市场交易、跨游戏复用（视项目设计而定）。</td></tr><tr><td><strong>教育 / 职业证书</strong></td><td>学历、资格证上链，用人单位可即时验证，防假。</td></tr><tr><td><strong>现实资产（RWA）</strong></td><td>房产等实物「代币化」上链，可拆分所有权，多人共同投资同一资产。</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五普及金融服务">五、普及金融服务<a href="https://blog.kuizuo.me/blog/blockchain-applications#%E4%BA%94%E6%99%AE%E5%8F%8A%E9%87%91%E8%9E%8D%E6%9C%8D%E5%8A%A1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>全球很多人<strong>没有银行账户</strong>或<strong>银行服务很差</strong>——存不了钱、贷不到款、碰不到投资产品。</p>
<p><strong>DeFi + 智能手机 + 网络</strong>就能用：链上可以<strong>存钱生息、借贷、交易</strong>，不依赖线下网点、不先看你的「社会经济地位」。以前只有华尔街能玩的工具，现在有网就能接触（风险自担）。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六dao去中心化治理">六、DAO：去中心化治理<a href="https://blog.kuizuo.me/blog/blockchain-applications#%E5%85%ADdao%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E6%B2%BB%E7%90%86" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>DAO（去中心化自治组织）</strong> = 用链上<strong>提案 + 投票 + 自动执行</strong>来做决策，像「互联网原生的俱乐部或公司」。</p>
<ul>
<li>成员用<strong>代币</strong>对提案投票，决定规则和资金怎么花。</li>
<li>从提案、投票到执行，<strong>全流程上链</strong>，智能合约按结果自动执行，减少「中心说了算」和黑箱操作。</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七关键术语速览">七、关键术语速览<a href="https://blog.kuizuo.me/blog/blockchain-applications#%E4%B8%83%E5%85%B3%E9%94%AE%E6%9C%AF%E8%AF%AD%E9%80%9F%E8%A7%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<table><thead><tr><th>概念</th><th>一句话</th></tr></thead><tbody><tr><td><strong>DApp</strong></td><td>后端逻辑跑在区块链上的应用（网站或 App），而不是某一家公司的服务器；数据与规则由链保证。</td></tr><tr><td><strong>Web1</strong></td><td>早期互联网，静态、「只读」网页。</td></tr><tr><td><strong>Web2</strong></td><td>现在的「读写」互联网，互动、社交多，但数据和内容多由大平台控制。</td></tr><tr><td><strong>Web3</strong></td><td>基于区块链的下一代方向：智能合约 + 数字所有权，用户更直接地掌控自己的数据和资产。</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八常见质疑的回应">八、常见质疑的回应<a href="https://blog.kuizuo.me/blog/blockchain-applications#%E5%85%AB%E5%B8%B8%E8%A7%81%E8%B4%A8%E7%96%91%E7%9A%84%E5%9B%9E%E5%BA%94" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<table><thead><tr><th>质疑</th><th>简要回应</th></tr></thead><tbody><tr><td><strong>「区块链不是给犯罪分子用的吗？」</strong></td><td>链上交易<strong>公开、可追溯</strong>，匿名犯罪并不方便；技术中性，正面用途（如给无银行账户的人提供金融服务）与滥用可以分开看，监管也在跟进。</td></tr><tr><td><strong>「不是特别耗能、不环保吗？」</strong></td><td>高耗能主要针对<strong>早期 PoW 链</strong>（如部分时期的比特币）。很多新链和以太坊「合并」升级后，能耗已大幅下降，有的甚至低于传统银行或大型科技公司。</td></tr><tr><td><strong>「对普通人来说太难了吧？」</strong></td><td>底层复杂，但<strong>用起来</strong>不必懂协议。就像大家用互联网不必懂 TCP/IP 一样，很多 DApp 正在把体验做得更简单，用会钱包、会点按钮就能参与。</td></tr></tbody></table>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="区块链" term="区块链"/>
        <category label="DeFi" term="DeFi"/>
        <category label="NFT" term="NFT"/>
        <category label="DAO" term="DAO"/>
        <category label="Web3" term="Web3"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[区块链宇宙]]></title>
        <id>https://blog.kuizuo.me/blog/blockchain-universe</id>
        <link href="https://blog.kuizuo.me/blog/blockchain-universe"/>
        <updated>2026-02-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[多链世界概览：为什么会有这么多链、L1 和 L2 是啥、主网和测试网怎么用、链 ID 干什么用。]]></summary>
        <content type="html"><![CDATA[<p><strong>驾驭多链世界：从 L1、L2 到主网与测试网</strong></p>
<p>说人话就是：刚进 Web3 很容易以为「区块链」就一条链，其实是一整个<strong>多链世界</strong>——除了比特币、以太坊，还有 zkSync、Solana、Arbitrum、Polygon、Optimism 等一大堆。<strong>这些链为啥这么多、怎么分层、主网和测试网又是什么？</strong> 下面用「可扩展性 → L1/L2 → 主网/测试网 → 链 ID」顺一遍，给你一张简明的宇宙地图。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一可扩展性问题为什么需要这么多链">一、可扩展性问题：为什么需要这么多链？<a href="https://blog.kuizuo.me/blog/blockchain-universe#%E4%B8%80%E5%8F%AF%E6%89%A9%E5%B1%95%E6%80%A7%E9%97%AE%E9%A2%98%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E8%BF%99%E4%B9%88%E5%A4%9A%E9%93%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>主要驱动力是<strong>可扩展性</strong>。以太坊等链的处理能力有限（例如每秒十几到几十笔交易），人一多就堵、手续费飞涨。</p>
<p><strong>举个栗子</strong>：2021 年 NFT 热的时候，很多人挤在以太坊上 mint、买卖 NFT。可以把以太坊想成一条进城的<strong>高速公路</strong>——高峰时堵车、速度慢、打车费暴涨。链上也一样：<strong>拥堵 → 交易变慢、Gas 费飙高</strong>，转 50 美元却要付 200 美元手续费并不稀奇，日常根本没法用。</p>
<p>所以社区在找办法扩容。思路说白了就是：<strong>多修路</strong>——要么建新高速（<strong>第一层，L1</strong>），要么在现有高速上加快速道（<strong>第二层，L2</strong>）。</p>
<p><strong>不可能三角：为啥不能「一条链全搞定」？</strong></p>
<p>背后有一个根本约束，叫<strong>不可能三角</strong>：<strong>去中心化、安全性、可扩展性</strong>三者很难同时拉满，通常只能优先保证其中两个。</p>
<table><thead><tr><th>维度</th><th>含义</th><th>典型取舍</th></tr></thead><tbody><tr><td><strong>去中心化</strong></td><td>节点分散、无单点控制</td><td>节点越多越安全，但共识慢、吞吐低</td></tr><tr><td><strong>安全性</strong></td><td>抗攻击、防篡改、共识可靠</td><td>要安全就要多验证、多冗余，成本高</td></tr><tr><td><strong>可扩展性</strong></td><td>高 TPS、低手续费、快确认</td><td>要快就要少节点或链下处理，往往牺牲部分去中心化或安全假设</td></tr></tbody></table>
<p>所以没有「一条链又去中心、又安全、又便宜又快」的完美方案，只能<strong>分工</strong>：有的链偏安全和去中心化（如以太坊 L1），有的偏速度和成本（如 L2、部分新公链），多链 + L2 本质是在三角里做不同取舍、再通过分层组合起来。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二修更多路l1-和-l2">二、修更多路：L1 和 L2<a href="https://blog.kuizuo.me/blog/blockchain-universe#%E4%BA%8C%E4%BF%AE%E6%9B%B4%E5%A4%9A%E8%B7%AFl1-%E5%92%8C-l2" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>「修更多路」体现在两类链上：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第一层l1区块链">第一层（L1）区块链<a href="https://blog.kuizuo.me/blog/blockchain-universe#%E7%AC%AC%E4%B8%80%E5%B1%82l1%E5%8C%BA%E5%9D%97%E9%93%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>L1</strong> = 从底层自己搭的、<strong>独立</strong>的区块链，例如 <strong>Solana、Avalanche</strong>。</li>
<li>相当于<strong>新修一条高速</strong>，用不同的设计和工程思路，追求比以太坊更快、更便宜。</li>
<li>每条 L1 有自己的<strong>安全模型、开发者生态和社区</strong>。</li>
</ul>
<p>学不同 L1 有点像学不同语言：先把<strong>以太坊</strong>搞懂（生态最大、最成熟），再看其他 L1 会轻松很多。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第二层l2区块链">第二层（L2）区块链<a href="https://blog.kuizuo.me/blog/blockchain-universe#%E7%AC%AC%E4%BA%8C%E5%B1%82l2%E5%8C%BA%E5%9D%97%E9%93%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>L2</strong> = <strong>搭在已有 L1（多数是以太坊）之上</strong>的链，例如 <strong>Arbitrum、Optimism、zkSync</strong>。</li>
<li>相当于在现有以太坊「主干道」上面加<strong>快速通道</strong>：在链下处理大量交易、成本低，再把压缩后的结果<strong>批量提交回 L1</strong>。</li>
<li>这样既<strong>继承以太坊的安全和去中心化</strong>，又获得<strong>更快的速度和更低的费用</strong>。</li>
</ul>
<table><thead><tr><th>类型</th><th>比喻</th><th>代表</th><th>特点</th></tr></thead><tbody><tr><td><strong>L1</strong></td><td>新修的高速</td><td>以太坊、Solana、Avalanche</td><td>独立共识与安全，从零构建</td></tr><tr><td><strong>L2</strong></td><td>主干道上的快速道</td><td>Arbitrum、Optimism、zkSync、Polygon</td><td>依托 L1 安全，链下执行、批量回传，更快更便宜</td></tr></tbody></table>
<p><strong>结构一览</strong>（以以太坊为中心的多链格局）：</p>
<div class="language-javascript codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-javascript codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">                区块链基础设施</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">                   </span><span class="token maybe-class-name">Ethereum</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">                      │</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">          ┌───────────┼───────────┐</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">          │                       │</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    </span><span class="token maybe-class-name">Layer2</span><span class="token plain"> 扩容              </span><span class="token function" style="color:hsl(221, 87%, 60%)">新公链</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">(</span><span class="token constant" style="color:hsl(35, 99%, 36%)">L1</span><span class="token punctuation" style="color:hsl(119, 34%, 47%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">          │                       │</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">   ┌──────┼──────┬──────┐         │</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">   │      │      │      │         │</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">zkSync  </span><span class="token maybe-class-name">Arbitrum</span><span class="token plain">  </span><span class="token maybe-class-name">Optimism</span><span class="token plain">      </span><span class="token maybe-class-name">Solana</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">   │      │      │</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain"></span><span class="token constant" style="color:hsl(35, 99%, 36%)">ZK</span><span class="token plain"> </span><span class="token maybe-class-name">Rollup</span><span class="token plain">  </span><span class="token maybe-class-name">Optimistic</span><span class="token plain">  </span><span class="token maybe-class-name">Optimistic</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<ul>
<li><strong>左侧（L2）</strong>：建在以太坊之上的扩容链——zkSync 属 <strong>ZK Rollup</strong>，Arbitrum、Optimism 属 <strong>Optimistic Rollup</strong>，都是「链下执行、结果回传 L1」。</li>
<li><strong>右侧（L1）</strong>：与以太坊并列的<strong>新公链</strong>，如 Solana，自己独立共识、从零构建。</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="rollup-是什么zk-和-optimistic-有啥区别">Rollup 是什么？ZK 和 Optimistic 有啥区别？<a href="https://blog.kuizuo.me/blog/blockchain-universe#rollup-%E6%98%AF%E4%BB%80%E4%B9%88zk-%E5%92%8C-optimistic-%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p><strong>Rollup</strong> = 把大量交易在<strong>链下</strong>执行完，只把「结果摘要」<strong>打包提交回 L1</strong>。这样 L1 不用逐笔验证，吞吐量上去、手续费下来，同时安全仍由 L1 保障。可以理解成：活在外面干，只回总部「交作业」。</p>
<p>L2 里常见两种 Rollup，区别在于<strong>怎么向 L1 证明「这批交易没问题」</strong>：</p>
<table><thead><tr><th>类型</th><th>思路</th><th>代表</th><th>一句话</th></tr></thead><tbody><tr><td><strong>Optimistic Rollup</strong></td><td>先默认你是对的，<strong>有人质疑再验</strong>。有一段时间（如 7 天）「挑战期」，没人举证欺诈就最终确认。</td><td>Arbitrum、Optimism</td><td>乐观假设没问题，出事再算账；确认慢一点，实现相对简单。</td></tr><tr><td><strong>ZK Rollup</strong></td><td>每次提交都带一份<strong>零知识证明</strong>，用数学证明「这批交易按规则执行了」，L1 只验证明、不重算交易。</td><td>zkSync、StarkNet</td><td>用证明说话，L1 秒验；确认快、隐私更好，但技术门槛高。</td></tr></tbody></table>
<p>简单记：<strong>Optimistic = 先信你，有异议再查；ZK = 每次交数学证明，链上只验证明。</strong></p>
<p><strong>零知识证明（Zero-Knowledge Proof）是啥？</strong></p>
<p>= 你能向对方证明「某件事成立」，但<strong>不用把这件事的具体内容摊开给他看</strong>。对方只要验证你给的「证明」就能确信结果是对的，却仍然不知道背后的细节——所以叫「零知识」：<strong>证明为真，却不泄露知识</strong>。</p>
<p><strong>举个栗子</strong>：你想证明自己知道某扇门的密码，又不想把密码说出来。零知识证明的做法是：你当着对方的面用密码开门（或完成某种只有知道密码才能完成的验证），对方看到「门确实开了」就信你知道密码，但自始至终没看到密码本身。在 ZK Rollup 里也一样：L2 交给 L1 的是一份<strong>数学证明</strong>，L1 验证这份证明就能确信「这批交易按规则执行了」，却不用把每笔交易重新算一遍，既省算力又保护隐私。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三主网和测试网真金白银-vs-练习场">三、主网和测试网：真金白银 vs 练习场<a href="https://blog.kuizuo.me/blog/blockchain-universe#%E4%B8%89%E4%B8%BB%E7%BD%91%E5%92%8C%E6%B5%8B%E8%AF%95%E7%BD%91%E7%9C%9F%E9%87%91%E7%99%BD%E9%93%B6-vs-%E7%BB%83%E4%B9%A0%E5%9C%BA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>和任意一条链交互时，你会碰到两种环境：<strong>主网</strong>和<strong>测试网</strong>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="主网mainnet">主网（Mainnet）<a href="https://blog.kuizuo.me/blog/blockchain-universe#%E4%B8%BB%E7%BD%91mainnet" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>主网</strong> = 真实在跑的、<strong>有真实经济价值</strong>的公共链。</li>
<li>在主网上转币、用 DApp，动的是<strong>真钱</strong>；每笔交易要付<strong>真实手续费</strong>，操作错了<strong>不可逆</strong>。</li>
<li>可以类比成：拿真金白银上牌桌。</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="测试网testnet">测试网（Testnet）<a href="https://blog.kuizuo.me/blog/blockchain-universe#%E6%B5%8B%E8%AF%95%E7%BD%91testnet" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>测试网</strong> = 和主网<strong>协议一致</strong>的平行环境，但用的是<strong>没有实际价值</strong>的「假币」。</li>
<li>开发者可以在上面部署、调试智能合约；用户也可以随便玩 DApp，<strong>不怕亏钱</strong>。</li>
<li>要拿测试网代币，一般通过<strong>水龙头（Faucet）</strong>：在指定网站填钱包地址，领免费测试币。目前以太坊常用的测试网之一是 <strong>Sepolia</strong>。</li>
</ul>
<table><thead><tr><th>环境</th><th>资产</th><th>用途</th></tr></thead><tbody><tr><td><strong>主网</strong></td><td>有真实价值</td><td>真实交易、真实资金与合约</td></tr><tr><td><strong>测试网</strong></td><td>无真实价值（水龙头领取）</td><td>开发测试、体验 DApp 无风险</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四链-id别连错网">四、链 ID：别连错网<a href="https://blog.kuizuo.me/blog/blockchain-universe#%E5%9B%9B%E9%93%BE-id%E5%88%AB%E8%BF%9E%E9%94%99%E7%BD%91" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>链一多，钱包和 DApp 怎么知道当前连的是哪条网？靠 <strong>链 ID（Chain ID）</strong>。</p>
<ul>
<li><strong>链 ID</strong> = 给每条链分配的一个<strong>唯一数字</strong>，用来标识网络。</li>
<li>钱包靠它保证：你签的每笔交易发到<strong>正确的链</strong>，避免把主网资产误操作到测试网（或反过来）。</li>
</ul>
<p><strong>常见例子</strong>：</p>
<ul>
<li><strong>以太坊主网</strong> 链 ID：<code>1</code></li>
<li><strong>Sepolia 测试网</strong> 链 ID：<code>11155111</code></li>
</ul>
<p>各条链的链 ID 和接入信息可以到 <a href="https://chainlist.org/" target="_blank" rel="noopener noreferrer">chainlist.org</a> 等站点查。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五总结">五、总结<a href="https://blog.kuizuo.me/blog/blockchain-universe#%E4%BA%94%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<ul>
<li><strong>为啥这么多链</strong>：单链<strong>可扩展性</strong>有限，一堵就慢、就贵；背后是<strong>不可能三角</strong>（去中心化、安全性、可扩展性难兼得），所以只能分工——有的偏安全与去中心化，有的偏快和便宜。</li>
<li><strong>L1 和 L2</strong>：<strong>L1</strong> = 独立底层链（以太坊、Solana 等）；<strong>L2</strong> = 建在 L1 上的扩容链（Arbitrum、Optimism、zkSync 等），链下执行、结果回传 L1，继承安全又提速降费。</li>
<li><strong>Rollup 两种</strong>：<strong>Optimistic</strong> = 先默认对，有异议再验（挑战期）；<strong>ZK</strong> = 每次交零知识证明，L1 只验证明。<strong>零知识证明</strong> = 证明「某事成立」却不泄露具体内容。</li>
<li><strong>主网 vs 测试网</strong>：<strong>主网</strong> = 真钱、真 Gas、操作不可逆；<strong>测试网</strong> = 假币、水龙头领、练手用（如 Sepolia）。</li>
<li><strong>链 ID</strong>：每条链一个<strong>唯一数字</strong>，钱包靠它保证连对网，避免主网资产误发到测试网。</li>
</ul>
<p>多链、L1、L2、主网、测试网、链 ID 这些概念不用一次全背，用多了自然会熟。</p>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="区块链" term="区块链"/>
        <category label="L1" term="L1"/>
        <category label="L2" term="L2"/>
        <category label="主网" term="主网"/>
        <category label="测试网" term="测试网"/>
        <category label="Web3" term="Web3"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[区块链预言机]]></title>
        <id>https://blog.kuizuo.me/blog/blockchain-oracle</id>
        <link href="https://blog.kuizuo.me/blog/blockchain-oracle"/>
        <updated>2026-02-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[链上拿不到链下数据怎么办？预言机问题从哪来、为啥不能直接调 API、DON 与混合智能合约又是什么。]]></summary>
        <content type="html"><![CDATA[<p><strong>链上很安全，但拿不到链下数据——预言机是咋解决这个问题的？</strong></p>
<p>说人话就是：区块链和智能合约只能看<strong>链上</strong>已有的数据（余额、转账、合约状态），<strong>碰不到链外</strong>的天气、价格、比赛结果、物流状态等。可很多应用偏偏要这些「现实世界数据」才能跑起来。<strong>那链上怎么安全地用上链下数据？</strong> 这就是<strong>预言机问题</strong>；解决它的方式叫<strong>预言机</strong>，把链下数据「搬」上链给合约用。下面从「为啥链拿不到链下 → 为啥不能直接调 API → 预言机与 DON → 混合智能合约」顺一遍。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一链的孤立与预言机问题">一、链的「孤立」与预言机问题<a href="https://blog.kuizuo.me/blog/blockchain-oracle#%E4%B8%80%E9%93%BE%E7%9A%84%E5%AD%A4%E7%AB%8B%E4%B8%8E%E9%A2%84%E8%A8%80%E6%9C%BA%E9%97%AE%E9%A2%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>区块链是<strong>去中心化、无需信任</strong>的共享账本；智能合约是链上<strong>按代码自动执行</strong>的协议，不依赖中间人。两者一起搭了一个安全、隔离的数字世界——但<strong>隔离</strong>本身也是限制：链和合约<strong>只能读写链上已有数据</strong>，不能主动去抓<strong>链下</strong>（现实世界）的数据。</p>
<p>所以会出现这种需求：</p>
<table><thead><tr><th>场景</th><th>需要的链下数据</th></tr></thead><tbody><tr><td>天气/灾害险</td><td>某地是否发生洪水、温度等，用于自动理赔</td></tr><tr><td>DeFi 借贷</td><td>实时价格（股票、加密货币）做抵押与清算</td></tr><tr><td>预测市场</td><td>比赛比分、选举结果等，用于结算</td></tr><tr><td>供应链</td><td>包裹是否签收、物流状态</td></tr></tbody></table>
<p>拿不到这些数据，智能合约就只能做「纯链上」的简单逻辑（比如转币）。<strong>如何把链下数据安全、可信地送进链上给合约用</strong>，就是<strong>预言机问题</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二为啥不能直接调-api确定性">二、为啥不能直接调 API？——确定性<a href="https://blog.kuizuo.me/blog/blockchain-oracle#%E4%BA%8C%E4%B8%BA%E5%95%A5%E4%B8%8D%E8%83%BD%E7%9B%B4%E6%8E%A5%E8%B0%83-api%E7%A1%AE%E5%AE%9A%E6%80%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>你可能会想：合约直接调天气网站、交易所的 API 不行吗？问题出在区块链的<strong>确定性</strong>。</p>
<ul>
<li><strong>确定性</strong> = 同一输入，所有节点算出的结果必须<strong>完全一致</strong>，这样全网才能达成共识、状态一致。</li>
<li><strong>链下数据</strong> = 会随时间变（价格、温度、比分），不同节点在不同时刻请求 API 可能拿到<strong>不同结果</strong>（例如 25°C 和 25.1°C），节点间状态就会分歧，<strong>共识崩掉</strong>。</li>
</ul>
<p>所以链上逻辑不能「各自去调 API」——必须通过一套<strong>约定好的方式</strong>，把「唯一、可共识」的数据写进链，再由合约读取。这套「把链下数据变成链上可用的可信输入」的机制，就是<strong>预言机</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三预言机桥接链上与链下">三、预言机：桥接链上与链下<a href="https://blog.kuizuo.me/blog/blockchain-oracle#%E4%B8%89%E9%A2%84%E8%A8%80%E6%9C%BA%E6%A1%A5%E6%8E%A5%E9%93%BE%E4%B8%8A%E4%B8%8E%E9%93%BE%E4%B8%8B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>预言机（Oracle）</strong> = 把<strong>链下数据</strong>获取、校验后<strong>写入链上</strong>，供智能合约使用的服务或网络。可以理解成「<strong>链上与现实世界之间的桥</strong>」。</p>
<p>若只用<strong>一个中心化预言机</strong>（一家公司、一个 API），就重新引入了<strong>单点故障和信任</strong>：被黑、宕机、作恶改数据，合约就全信了。所以和区块链一样，预言机也要<strong>去中心化</strong>才靠谱——于是有了<strong>去中心化预言机网络（DON）</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四去中心化预言机网络don">四、去中心化预言机网络（DON）<a href="https://blog.kuizuo.me/blog/blockchain-oracle#%E5%9B%9B%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E9%A2%84%E8%A8%80%E6%9C%BA%E7%BD%91%E7%BB%9Cdon" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>DON（Decentralized Oracle Network）</strong> = 由<strong>多个独立预言机节点</strong>组成的网络，一起提供链下数据并达成共识后再上链。</p>
<p>大致流程：</p>
<ol>
<li><strong>多节点</strong>从链下（多个数据源）抓同一类数据（如 ETH 价格）。</li>
<li><strong>交叉核对</strong>，节点之间比对数据。</li>
<li><strong>共识</strong>：用协议对「一个公认的正确值」达成一致。</li>
<li><strong>上链</strong>：把验证过的结果提交到链上，合约按这个值执行。</li>
</ol>
<p>这样数据<strong>可验证、抗单点故障、抗篡改</strong>，链上合约依赖的是「一整套去中心化预言机」而不是某一家公司。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五混合智能合约">五、混合智能合约<a href="https://blog.kuizuo.me/blog/blockchain-oracle#%E4%BA%94%E6%B7%B7%E5%90%88%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>链上逻辑 + 链下可靠数据</strong> = <strong>混合智能合约</strong>。</p>
<table><thead><tr><th>部分</th><th>在哪</th><th>干啥</th></tr></thead><tbody><tr><td><strong>链上</strong></td><td>区块链</td><td>合约核心逻辑：规则、清算、结算；去中心化、透明、不可篡改</td></tr><tr><td><strong>链下</strong></td><td>DON 等</td><td>现实世界数据与计算：价格、天气、随机数、跨链消息等</td></tr></tbody></table>
<p>这样既保留区块链的<strong>安全与透明</strong>，又用到<strong>现实世界的丰富数据</strong>。现在很多 DeFi、链游、保险、供应链项目都是这种「链上合约 + 预言机喂数」的混合架构；日常说的「智能合约」很多其实已经是这种混合模型。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六chainlink预言机网络的一个代表">六、Chainlink：预言机网络的一个代表<a href="https://blog.kuizuo.me/blog/blockchain-oracle#%E5%85%ADchainlink%E9%A2%84%E8%A8%80%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9A%84%E4%B8%80%E4%B8%AA%E4%BB%A3%E8%A1%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>Chainlink</strong> 是常用的<strong>去中心化预言机网络</strong>之一，和具体哪条链解耦，可以给以太坊、Avalanche、Polygon、zkSync 等多条链喂数。了解它有助于理解「预言机在实际里长什么样」。</p>
<p>常见能力包括：</p>
<ul>
<li><strong>数据源（Price Feeds 等）</strong>：把<strong>实时市场价格</strong>等可靠数据喂给 DeFi（清算、抵押率等）。</li>
<li><strong>自动化</strong>：按<strong>时间或事件</strong>触发合约执行，不依赖中心化服务器（如定时任务、条件触发）。</li>
<li><strong>VRF（可验证随机函数）</strong>：提供<strong>可验证的随机数</strong>，链本身无法生成真随机，链游、抽奖、NFT 等会用到。</li>
<li><strong>跨链通信</strong>：让一条链上的合约<strong>给另一条链上的合约发消息</strong>，做跨链协作。</li>
</ul>
<p>预言机把「链下数据 + 触发 + 随机数 + 跨链」等能力标准化后提供给链上，智能合约才能和现实世界、多链环境安全交互。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七总结">七、总结<a href="https://blog.kuizuo.me/blog/blockchain-oracle#%E4%B8%83%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<ul>
<li><strong>预言机问题</strong>：链和合约只能读链上数据，拿不到链下（价格、天气、比分等），很多应用需要这些数据才能跑。</li>
<li><strong>不能直接调 API</strong>：区块链要<strong>确定性</strong>，各节点结果必须一致；链下数据会变，各自调 API 会导致节点分歧、共识崩溃。</li>
<li><strong>预言机</strong>：把链下数据<strong>获取、校验、共识后写进链上</strong>，供合约使用；<strong>中心化预言机</strong>有单点故障，所以需要 <strong>DON（去中心化预言机网络）</strong>。</li>
<li><strong>混合智能合约</strong>：链上逻辑 + 链下数据（由 DON 等提供），既安全又可对接现实世界；DeFi、链游、保险等大量采用。</li>
<li><strong>Chainlink</strong> 等 DON：提供价格数据、自动化、VRF、跨链等能力，是「链与现实世界、多链之间」安全交互的一类基础设施。</li>
</ul>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="区块链" term="区块链"/>
        <category label="预言机" term="预言机"/>
        <category label="智能合约" term="智能合约"/>
        <category label="Web3" term="Web3"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[智能合约的目的]]></title>
        <id>https://blog.kuizuo.me/blog/purpose-of-smart-contracts</id>
        <link href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts"/>
        <updated>2026-02-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[从「承诺为何总被打破」到「用代码写死规则」——智能合约如何用不可篡改、去中心化与透明度取代对人的信任。]]></summary>
        <content type="html"><![CDATA[<p><strong>从「拉钩约定」到「代码即规则」</strong></p>
<p>现代生活几乎都建立在<strong>承诺</strong>上：修车、买保险、全球供应链，本质都是一份份协议。问题是这些承诺经常被打破——<strong>那有没有一种协议，不用信人、只信规则，且无法被单方篡改或关停？</strong> 这就是<strong>智能合约</strong>要回答的事。下面从「问题 → 方案 → 三大支柱 → 现实应用 → 常见误解」顺一遍。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一问题为什么承诺如此容易被违背">一、问题：为什么承诺如此容易被违背？<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#%E4%B8%80%E9%97%AE%E9%A2%98%E4%B8%BA%E4%BB%80%E4%B9%88%E6%89%BF%E8%AF%BA%E5%A6%82%E6%AD%A4%E5%AE%B9%E6%98%93%E8%A2%AB%E8%BF%9D%E8%83%8C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>现行协议依赖我们对<strong>中心化机构</strong>（公司、银行、政府）会履行义务的信任，这就带来一个核心风险：<strong>交易对手风险</strong>——对方违约的风险。</p>
<p>为什么屡屡出事？因为这些机构由逐利的人掌控，一旦<strong>钻规则空子或直接违约</strong>能带来利益，诱惑往往难以抵挡。历史上反复上演：</p>
<table><thead><tr><th>案例</th><th>承诺是什么</th><th>为何被打破</th></tr></thead><tbody><tr><td><strong>2008 年金融危机</strong></td><td>金融机构「负责任地」管理风险</td><td><strong>中心化</strong>机构做不透明幕后交易，制造明知风险极高的复杂产品，隐性承诺被粉碎</td></tr><tr><td><strong>麦当劳大富翁骗局</strong></td><td>顾客有公平机会中奖</td><td>负责游戏的公司<strong>内部人员</strong>操纵系统、窃取奖金；游戏<strong>中央控制且不透明</strong>，公平形同虚设</td></tr><tr><td><strong>Robinhood 暂停交易</strong></td><td>为散户提供公平市场准入</td><td><strong>中心化</strong>平台拥有「开关」，在 GME/AMC 暴涨时单方暂停交易，剥夺用户参与权</td></tr><tr><td><strong>银行挤兑</strong></td><td>存款安全、随时可提</td><td>银行把存款贷出后没有足够现金储备，<strong>违约结构</strong>在挤兑时暴露，系统崩溃</td></tr></tbody></table>
<p>共同点：失败根源都在于<strong>不得不信任一个为自身利益行事的中心化机构</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二解决方案用智能合约创造牢不可破的承诺">二、解决方案：用智能合约创造「牢不可破」的承诺<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#%E4%BA%8C%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%E7%94%A8%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%88%9B%E9%80%A0%E7%89%A2%E4%B8%8D%E5%8F%AF%E7%A0%B4%E7%9A%84%E6%89%BF%E8%AF%BA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果我们能写出<strong>无需信任对方、也无需信任公司</strong>的协议，且保证<strong>完全按约定执行、无法篡改或审查</strong>呢？这正是<strong>智能合约</strong>与区块链的根本目的。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="智能合约是什么">智能合约是什么？<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>定义</strong>：用<strong>代码</strong>编写、部署在<strong>去中心化区块链</strong>上的协议；是一组在<strong>特定条件满足时自动执行</strong>的指令。</li>
<li><strong>比喻</strong>：像一台<strong>数字自动售货机</strong>——你投入「代币」（数据或价值），机器按程序自动给出「商品」（结果），<strong>没有任何中间人决定要不要履约</strong>。</li>
</ul>
<p>把这样的代码放在去中心化网络上运行，就得到一种新型协议：<strong>信任最小化</strong>、在实践中<strong>不可破坏</strong>的承诺。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三无需信任协议的三大支柱">三、无需信任协议的三大支柱<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#%E4%B8%89%E6%97%A0%E9%9C%80%E4%BF%A1%E4%BB%BB%E5%8D%8F%E8%AE%AE%E7%9A%84%E4%B8%89%E5%A4%A7%E6%94%AF%E6%9F%B1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>智能合约的能力来自区块链自带的三个特性：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-不可篡改性">1. 不可篡改性<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#1-%E4%B8%8D%E5%8F%AF%E7%AF%A1%E6%94%B9%E6%80%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>合约一旦<strong>部署上链</strong>，代码在密码学上<strong>无法被修改</strong>。</li>
<li>若麦当劳大富翁的规则写进智能合约，内部人员在数学上就<strong>无法篡改</strong>开奖结果，规则被永久锁定。</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-去中心化">2. 去中心化<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#2-%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>合约不跑在某一家公司的服务器上，而是由<strong>全球数千台独立节点</strong>执行和验证。</li>
<li>没有中央「关闭开关」——任何个人或公司（包括类似 Robinhood 的平台）都<strong>无法单方关停</strong>合约，整个网络共同保证其按规则持续运行。</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-透明度">3. 透明度<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#3-%E9%80%8F%E6%98%8E%E5%BA%A6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li>合约代码通常<strong>公开可查</strong>，任何人都能审计规则。</li>
<li>2008 年那种「幕后交易」在链上难以隐藏；参与前即可<strong>验证协议是否公平</strong>。</li>
</ul>
<p>三者叠加：你不再需要信任对方的品牌或意图，只需信任<strong>代码里可验证的逻辑</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四从品牌信任到数学保障">四、从「品牌信任」到「数学保障」<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#%E5%9B%9B%E4%BB%8E%E5%93%81%E7%89%8C%E4%BF%A1%E4%BB%BB%E5%88%B0%E6%95%B0%E5%AD%A6%E4%BF%9D%E9%9A%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<table><thead><tr><th>维度</th><th>传统体系（品牌信任）</th><th>智能合约 / Web3（数学信任）</th></tr></thead><tbody><tr><td><strong>依据</strong></td><td>纸面担保、名气、历史</td><td><strong>密码学保证</strong>、可验证、确定性</td></tr><tr><td><strong>信任对象</strong></td><td>银行、公司、人</td><td><strong>代码 + 去中心化网络</strong></td></tr><tr><td><strong>交易对手风险</strong></td><td>高，规则可被暗中改变</td><td><strong>低</strong>，规则上链、透明</td></tr><tr><td><strong>承诺有效性</strong></td><td>取决于「谁」在做承诺</td><td>取决于<strong>写死的规则</strong>是否被执行</td></tr></tbody></table>
<p>传统金融依赖你<strong>信这家机构</strong>；由智能合约驱动的世界依赖你<strong>信数学和代码</strong>——无需认识写代码的人，只需验证系统逻辑并由网络强制执行。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五智能合约如何解决现实问题">五、智能合约如何解决现实问题？<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#%E4%BA%94%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E7%8E%B0%E5%AE%9E%E9%97%AE%E9%A2%98" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-交易不被关停dex-替代中心化交易所">1. 交易不被关停：DEX 替代中心化交易所<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#1-%E4%BA%A4%E6%98%93%E4%B8%8D%E8%A2%AB%E5%85%B3%E5%81%9Cdex-%E6%9B%BF%E4%BB%A3%E4%B8%AD%E5%BF%83%E5%8C%96%E4%BA%A4%E6%98%93%E6%89%80" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>Robinhood 式「说停就停」的问题，在 <strong>Uniswap</strong> 等 <strong>DEX（去中心化交易所）</strong> 上被化解：市场完全由智能合约驱动，没有一家公司能<strong>暂停交易、冻结资金或拒绝访问</strong>。规则写进不可篡改的合约，<strong>24/7 对所有人开放</strong>。DeFi 锁仓规模在几年内从不足 10 亿美元涨到超过 2000 亿美元，说明市场对这类<strong>信任最小化</strong>金融系统的需求巨大。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-公平可验证预言机--随机数">2. 公平可验证：预言机 + 随机数<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#2-%E5%85%AC%E5%B9%B3%E5%8F%AF%E9%AA%8C%E8%AF%81%E9%A2%84%E8%A8%80%E6%9C%BA--%E9%9A%8F%E6%9C%BA%E6%95%B0" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>若麦当劳大富翁跑在链上，智能合约可以接入<strong>去中心化预言机</strong>（例如 <strong>Chainlink 的 VRF</strong>）生成<strong>可验证、防篡改</strong>的随机中奖结果，过程透明、可审计，从机制上杜绝内部操纵。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六关于中心化出事的澄清ftx-不是智能合约的失败">六、关于「中心化出事」的澄清：FTX 不是智能合约的失败<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#%E5%85%AD%E5%85%B3%E4%BA%8E%E4%B8%AD%E5%BF%83%E5%8C%96%E5%87%BA%E4%BA%8B%E7%9A%84%E6%BE%84%E6%B8%85ftx-%E4%B8%8D%E6%98%AF%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E7%9A%84%E5%A4%B1%E8%B4%A5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>FTX 倒闭常被误读为「区块链/智能合约失败了」——<strong>恰恰相反</strong>。</p>
<ul>
<li><strong>FTX</strong> 是<strong>中心化</strong>公司，不透明、基于信任，本质是** Web2 公司**扮成 Web3。</li>
<li>它的崩盘是<strong>中心化信任机制</strong>的失败，而智能合约要解决的正是这类问题。</li>
<li>这类事件反而说明：<strong>真正去中心化、链上透明、规则上链</strong>的 DeFi，比「把资产交给一家公司」更值得追求。</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七总结">七、总结<a href="https://blog.kuizuo.me/blog/purpose-of-smart-contracts#%E4%B8%83%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<ul>
<li><strong>智能合约的目的</strong>：用<strong>可验证、基于数学的保证</strong>，取代易碎的、依赖人的承诺；构建<strong>规则公平、透明、自动执行</strong>的系统，而不依赖中心机构的「善意」。</li>
<li><strong>三大支柱</strong>：<strong>不可篡改</strong>（规则写死）、<strong>去中心化</strong>（无单点关停）、<strong>透明度</strong>（可审计），共同降低<strong>交易对手风险</strong>。</li>
<li><strong>信任范式</strong>：从「信品牌、信机构」转向「<strong>信代码、信数学</strong>」；开发者和创造者可以用这套工具，做更公平、更抗单点故障的应用，把世界建在<strong>共享的、牢不可破的代码</strong>之上，而不是对机构的盲目信任上。</li>
</ul>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="区块链" term="区块链"/>
        <category label="智能合约" term="智能合约"/>
        <category label="DeFi" term="DeFi"/>
        <category label="Web3" term="Web3"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[什么是 EVM（以太坊虚拟机）]]></title>
        <id>https://blog.kuizuo.me/blog/what-is-evm</id>
        <link href="https://blog.kuizuo.me/blog/what-is-evm"/>
        <updated>2026-02-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[EVM 是什么、为何对去中心化至关重要，以及 EVM 等效与 EVM 兼容链的区别，给开发者一份简明参考。]]></summary>
        <content type="html"><![CDATA[<p><strong>从「计算引擎」到「等效 vs 兼容」</strong></p>
<p>了解以太坊时总会碰到 <strong>EVM</strong> 这个词——听起来很技术，但从宏观上搞懂它，对理解以太坊（以及一堆 L2、Rollup）怎么运作很重要。<strong>EVM 到底是什么、为什么说它是去中心化的基石、以及「EVM 等效」和「EVM 兼容」有啥区别？</strong> 下面用「定义 → 作用 → 等效/兼容 → 开发提示」顺一遍，目标是建立清晰概念，不追求底层实现细节。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一什么是-evm">一、什么是 EVM？<a href="https://blog.kuizuo.me/blog/what-is-evm#%E4%B8%80%E4%BB%80%E4%B9%88%E6%98%AF-evm" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>EVM（以太坊虚拟机）</strong> 是以太坊的<strong>计算引擎</strong>：驱动整条链「算账」和「执行合约」的那套规则和运行环境。</p>
<ul>
<li><strong>在哪运行</strong>：存在于每一个<strong>完整节点</strong>上，是一个<strong>全球共享的沙盒环境</strong>。</li>
<li><strong>干什么用</strong>：规定<strong>区块链状态</strong>如何从一个区块变到下一个区块，核心职责就是<strong>执行智能合约</strong>。</li>
</ul>
<p>可以把它想成两种东西：</p>
<table><thead><tr><th>类比</th><th>说明</th></tr></thead><tbody><tr><td><strong>操作系统</strong></td><td>像 Windows / macOS 负责跑各种软件一样，EVM 负责跑一种特定「软件」：<strong>智能合约</strong>。</td></tr><tr><td><strong>机器人厨师</strong></td><td>智能合约像一份精确的「食谱」，EVM 是严格按食谱做的「机器人厨师」。同样的输入（食谱 + 食材），每次都会得到<strong>完全相同的输出</strong>。</td></tr></tbody></table>
<p><strong>如果你熟悉 Java，还可以这样理解：EVM 和 JVM 很像。</strong></p>
<table><thead><tr><th>维度</th><th>JVM（Java 虚拟机）</th><th>EVM（以太坊虚拟机）</th></tr></thead><tbody><tr><td><strong>目标</strong></td><td>「写一次，到处跑」：同一份 Java 字节码在不同机器上结果一致</td><td>同一份智能合约在<strong>每个节点</strong>上执行结果一致，从而达成共识</td></tr><tr><td><strong>规范 vs 实现</strong></td><td>JVM 是规范，具体有 HotSpot、OpenJ9 等实现</td><td>EVM 是规范，具体由 Geth、Erigon 等客户端实现</td></tr><tr><td><strong>输入/输出</strong></td><td>输入字节码 + 数据，输出执行结果</td><td>输入合约字节码 + 交易数据，输出状态变更（确定性）</td></tr><tr><td><strong>运行环境</strong></td><td>单机或服务器，由一家/多台机器跑</td><td><strong>全球数千节点</strong>各自跑同一套 EVM，结果必须一致</td></tr></tbody></table>
<p>核心相似点：都是一层<strong>抽象出来的「虚拟机」</strong>，把「怎么执行」从具体硬件里抽离出来，保证<strong>同样的代码得到同样的结果</strong>。JVM 解决的是跨平台一致，EVM 在此基础上还要保证<strong>去中心化网络里所有节点一致</strong>，所以确定性要求更严。</p>
<p>这种<strong>可预测、确定性的执行</strong>，是 EVM 最核心的特征。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二为什么-evm-对去中心化至关重要">二、为什么 EVM 对去中心化至关重要？<a href="https://blog.kuizuo.me/blog/what-is-evm#%E4%BA%8C%E4%B8%BA%E4%BB%80%E4%B9%88-evm-%E5%AF%B9%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E8%87%B3%E5%85%B3%E9%87%8D%E8%A6%81" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>以太坊是<strong>去中心化</strong>的：由全球成千上万台<strong>节点</strong>各自维护同一套账本。用户发起一笔与智能合约的交互时，交易会被广播出去，<strong>每个节点都要独立执行这笔交易并验证结果</strong>。</p>
<p>这就带来一个关键问题：<strong>怎么保证这么多节点算出来的结果一模一样？</strong></p>
<p>EVM 的作用就在这里：它是一套<strong>统一的计算规范</strong>。每个节点跑的客户端里都实现了同一套 EVM 规则，执行同一份智能合约代码时，<strong>规则相同 → 结果相同</strong>。这样全网才能对「当前状态」达成<strong>共识</strong>，而不需要信任某一个中心。所以说，<strong>确定性执行</strong>是去中心化共识的基石。</p>
<p>补充一点：EVM 本身是<strong>规范</strong>（一套规则），真正跑起来的是<strong>客户端软件</strong>里的实现。例如 <strong>Geth（Go Ethereum）</strong> 里就包含严格按规范实现的 EVM，节点通过运行 Geth 等客户端来参与网络。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三evm-等效-vs-evm-兼容关键区别">三、EVM 等效 vs EVM 兼容：关键区别<a href="https://blog.kuizuo.me/blog/what-is-evm#%E4%B8%89evm-%E7%AD%89%E6%95%88-vs-evm-%E5%85%BC%E5%AE%B9%E5%85%B3%E9%94%AE%E5%8C%BA%E5%88%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>很多链（尤其是 <strong>Layer 2、Rollup</strong>）都「用 EVM」或「兼容 EVM」，但程度不同。区分<strong>等效</strong>和<strong>兼容</strong>很重要：</p>
<table><thead><tr><th>维度</th><th>EVM 等效（EVM-Equivalent）</th><th>EVM 兼容（EVM-Compatible）</th></tr></thead><tbody><tr><td><strong>目标</strong></td><td>在<strong>行为上</strong>与以太坊主网完全一致</td><td>能跑 EVM 系智能合约，但<strong>底层有差异或优化</strong></td></tr><tr><td><strong>机制</strong></td><td>底层机制、<strong>操作码</strong>、状态转换逻辑与主网相同</td><td>对核心架构有修改或优化，细节可能不同</td></tr><tr><td><strong>对开发者</strong></td><td>主网上的合约可<strong>直接部署</strong>，无需改代码、无需换工具，行为一致</td><td>多数代码能跑，但可能需要<strong>小改合约</strong>或用<strong>专用工具</strong>部署/交互</td></tr><tr><td><strong>例子</strong></td><td><strong>Optimism（OP）</strong>、<strong>Arbitrum</strong></td><td><strong>Polygon zkEVM</strong>、<strong>zkSync Era</strong></td></tr></tbody></table>
<p>开发前建议先确认：你要上的那条链是「等效」还是「兼容」，以及官方文档里写了哪些差异。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四给开发者的关键提示">四、给开发者的关键提示<a href="https://blog.kuizuo.me/blog/what-is-evm#%E5%9B%9B%E7%BB%99%E5%BC%80%E5%8F%91%E8%80%85%E7%9A%84%E5%85%B3%E9%94%AE%E6%8F%90%E7%A4%BA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>在以太坊生态（主网 + 各种 L2 / Rollup）里开发时：</p>
<ul>
<li><strong>务必查阅目标链的官方开发者文档</strong>，搞清楚该链与以太坊主网的细微差别。</li>
<li>这些差别会直接影响<strong>功能</strong>（例如某些 opcode 或预编译）和<strong>安全</strong>（例如 gas、重入、升级机制），不能默认「和主网完全一样」。</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五总结">五、总结<a href="https://blog.kuizuo.me/blog/what-is-evm#%E4%BA%94%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<ul>
<li><strong>EVM</strong> 是以太坊的<strong>计算引擎</strong>，定义状态如何随区块更新，负责<strong>执行智能合约</strong>；特点是<strong>确定性、可复现</strong>。</li>
<li><strong>对去中心化的意义</strong>：所有节点执行同一套 EVM 规则，得到相同结果，才能达成<strong>共识</strong>；实现上由 <strong>Geth</strong> 等客户端提供。</li>
<li><strong>EVM 等效</strong>：行为与主网一致，合约可无缝迁移（如 OP、Arbitrum）；<strong>EVM 兼容</strong>：能跑 Solidity 等合约，但底层有差异，部署前要查文档（如 Polygon zkEVM、zkSync Era）。</li>
</ul>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="区块链" term="区块链"/>
        <category label="以太坊" term="以太坊"/>
        <category label="EVM" term="EVM"/>
        <category label="智能合约" term="智能合约"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[智能合约的优势]]></title>
        <id>https://blog.kuizuo.me/blog/advantages-of-smart-contracts</id>
        <link href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts"/>
        <updated>2026-02-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[智能合约的几大优势：去中心化、透明可验证、伪匿名、效率与不可变性；并单独讲清可升级合约与 51% 攻击。]]></summary>
        <content type="html"><![CDATA[<p><strong>从「单点故障」到「信任最小化」</strong></p>
<p>在动手和链交互、领测试网代币、发交易之前，先搞清楚<strong>智能合约到底强在哪</strong>会很有帮助。这些优势大多直接来自底层的<strong>区块链</strong>：去中心化、透明、不可篡改、自动化。<strong>具体有哪些、和传统系统比差在哪？可升级合约和「代码不可改」矛盾吗？51% 攻击又是什么？</strong> 下面按「去中心化 → 透明 → 伪匿名 → 效率 → 不可变 → 可升级合约 → 51% 攻击 → 信任最小化」捋一遍。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一去中心化不存在单点故障">一、去中心化：不存在单点故障<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E4%B8%80%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E4%B8%8D%E5%AD%98%E5%9C%A8%E5%8D%95%E7%82%B9%E6%95%85%E9%9A%9C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>智能合约跑在<strong>去中心化</strong>的区块链上：不受任何单一实体或服务器控制。链由全球数千台<strong>节点</strong>维护，运行同一套规则、验证同一批交易；智能合约作为链上代码，自然继承这种特性。</p>
<ul>
<li><strong>传统中心化</strong>（如银行）：一家机构说了算，可以<strong>冻结资产</strong>、关停服务；中央服务器宕机，你就取不了钱——<strong>单点故障</strong>。</li>
<li><strong>链上智能合约</strong>：即使几百个节点离线，只要网络中还有节点在跑，合约就能继续按规则执行；没有「一个开关」能关掉整条链。</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二透明度和可验证性没有隐藏的意外">二、透明度和可验证性：没有隐藏的意外<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E4%BA%8C%E9%80%8F%E6%98%8E%E5%BA%A6%E5%92%8C%E5%8F%AF%E9%AA%8C%E8%AF%81%E6%80%A7%E6%B2%A1%E6%9C%89%E9%9A%90%E8%97%8F%E7%9A%84%E6%84%8F%E5%A4%96" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>在<strong>公共区块链</strong>上，操作和代码都是公开的。和合约交互前，你可以先看代码、搞清它在干什么：没有隐藏费用、没有模糊条款，也不用担心条款在你不知情时被改掉。</p>
<p>就像<strong>先看清规则再决定玩不玩</strong>，而且规则对所有人一样，且<strong>游戏过程中不能改</strong>。</p>
<p><strong>例子——保险</strong>：传统房屋险写明保野火，但保险公司可以在野火高发前<strong>单方修改条款</strong>，把野火排除在外，哪怕你已交多年保费。用智能合约 + <strong>预言机</strong>（如 Chainlink）：合约根据可验证的链下数据（如某地确已发生野火）自动触发赔付，条款<strong>写死在链上、不可单方修改</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三伪匿名透明与隐私的折中">三、伪匿名：透明与隐私的折中<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E4%B8%89%E4%BC%AA%E5%8C%BF%E5%90%8D%E9%80%8F%E6%98%8E%E4%B8%8E%E9%9A%90%E7%A7%81%E7%9A%84%E6%8A%98%E4%B8%AD" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>链上操作是<strong>透明的</strong>，但身份是<strong>伪匿名</strong>的：不直接绑你的真实姓名，而是用一个<strong>加密地址</strong>。该地址下的每笔交易都可查，但要把地址和具体的人对应起来，需要链外信息。这样既保留<strong>可验证性</strong>，又保留一定<strong>隐私</strong>。</p>
<p>需要更强隐私时，可以用 <strong>零知识证明（ZKP）</strong> 等技术：证明「某事成立」而不泄露具体内容。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四速度与效率247-自动执行">四、速度与效率：24/7 自动执行<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E5%9B%9B%E9%80%9F%E5%BA%A6%E4%B8%8E%E6%95%88%E7%8E%87247-%E8%87%AA%E5%8A%A8%E6%89%A7%E8%A1%8C" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>传统流程（如国际汇款）常要数天甚至数周，受节假日、时区、人工审批影响。智能合约在条件满足时<strong>自动执行</strong>，<strong>全年无休、无需人工或中介</strong>，显著缩短时间和成本。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五不可变性代码写死难以篡改">五、不可变性：代码写死、难以篡改<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E4%BA%94%E4%B8%8D%E5%8F%AF%E5%8F%98%E6%80%A7%E4%BB%A3%E7%A0%81%E5%86%99%E6%AD%BB%E9%9A%BE%E4%BB%A5%E7%AF%A1%E6%94%B9" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>智能合约<strong>部署上链后</strong>，代码在链上不可改、不可删，协议条款被<strong>永久锁定</strong>。这能避免「诱饵式」条款：一方先吸引你参与，再单方改条款谋利。链上代码一直在，规则对所有人透明。</p>
<p>下面两个概念常被问到：<strong>可升级智能合约</strong>（是不是和「不可变」矛盾？）和 <strong>51% 攻击</strong>（链到底多难被篡改？）——单独说清。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六可升级智能合约">六、可升级智能合约<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E5%85%AD%E5%8F%AF%E5%8D%87%E7%BA%A7%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>「代码不可改」指的是<strong>部署上去的那份字节码</strong>在链上不变；但可以通过<strong>设计模式</strong>实现「逻辑可升级」，即<strong>行为</strong>由另一份可替换的代码决定。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="是什么">是什么？<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p><strong>可升级智能合约</strong>是一种设计方式：有一个<strong>永久的入口合约</strong>（如代理），它自己不实现业务逻辑，而是把调用<strong>委托</strong>给另一个「实现合约」。要升级时，只需把入口指向新的实现合约地址，用户始终和同一个入口地址交互，但背后执行的逻辑已经换成了新版本。</p>
<p>可以类比成：<strong>路牌本身不动，但路牌指向的「目的地」可以换成新地址</strong>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="和不可篡改矛盾吗">和「不可篡改」矛盾吗？<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E5%92%8C%E4%B8%8D%E5%8F%AF%E7%AF%A1%E6%94%B9%E7%9F%9B%E7%9B%BE%E5%90%97" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>不矛盾</strong>。不可变的是「已部署的那段代码字节码」；可升级是<strong>事先写在合约里的规则</strong>——「谁可以改实现地址、在什么条件下改」都写在链上、可审计。</li>
<li>关键点：升级能力必须<strong>从一开始就设计进合约</strong>，用户能清楚看到「这个合约允许升级」以及「谁有权升级」。整个过程仍是<strong>透明、可验证</strong>的，不是项目方背地里改代码。</li>
</ul>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="常见实现方式了解即可">常见实现方式（了解即可）<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E5%B8%B8%E8%A7%81%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F%E4%BA%86%E8%A7%A3%E5%8D%B3%E5%8F%AF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<table><thead><tr><th>方式</th><th>简要说明</th></tr></thead><tbody><tr><td><strong>代理 + 实现分离</strong></td><td>用户调代理，代理把调用 delegatecall 到实现合约；换实现 = 改代理里存的实现地址</td></tr><tr><td><strong>透明代理 / UUPS</strong></td><td>升级逻辑放在代理或实现里，避免把升级和普通调用混在一起，减少误操作和攻击面</td></tr><tr><td><strong>升级权限</strong></td><td>通常由多签或 DAO 控制「谁可以改实现地址」，避免单点作恶</td></tr></tbody></table>
<p>对用户来说：用可升级合约的项目，要看清<strong>谁有升级权、有没有时间锁</strong>；真正的「不可变」是部署后连升级逻辑都没有的合约。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七51-攻击">七、51% 攻击<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E4%B8%8351-%E6%94%BB%E5%87%BB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>「不可篡改」依赖的是：<strong>想改历史，就要让全网接受你的版本</strong>，而在区块链里这等价于控制足够多的<strong>共识资源</strong>（算力或权益）。<strong>51% 攻击</strong>指的就是这种「控制多数资源从而试图篡改或作恶」的理论攻击。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="是什么-1">是什么？<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E6%98%AF%E4%BB%80%E4%B9%88-1" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>在<strong>工作量证明（PoW）<strong>链上（如早期比特币、以太坊 2.0 前），共识由</strong>算力</strong>决定：谁算得快、谁更容易产出被接受的区块。若某人或某集团控制了<strong>超过全网一半的算力</strong>，理论上可以：</p>
<ul>
<li><strong>双花</strong>：把同一笔钱花两次（先付给 A 并上链，再在私下挖一条更长链，把付给 A 的那笔交易去掉，把同一笔钱付给 B，再让全网接受新链）。</li>
<li><strong>拒绝打包</strong>：不打包某些交易，或审查特定地址。</li>
<li><strong>改写近期历史</strong>：在「短链」上重写最近几个区块，让原本已确认的交易被推翻。</li>
</ul>
<p>在<strong>权益证明（PoS）<strong>链上，对应的是控制超过一半的</strong>质押代币/验证者</strong>，逻辑类似：谁控制多数，谁在共识里说了算。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="为什么说几乎不可能">为什么说「几乎不可能」？<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AF%B4%E5%87%A0%E4%B9%8E%E4%B8%8D%E5%8F%AF%E8%83%BD" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>成本</strong>：在比特币、以太坊这种规模的网络上，要凑齐 51% 算力或质押，成本极高，且一旦链被攻击，代币价值可能崩盘，攻击者手里的资源也会贬值，<strong>不划算</strong>。</li>
<li><strong>设计目标</strong>：公链的设计目标就是让「控制多数」在现实中难以达成；节点分布越广、参与方越多，单方凑够 51% 越难。</li>
<li><strong>成熟链</strong>：在已经运行多年、算力/质押高度分散的链上，51% 攻击更多是<strong>理论风险</strong>，实践中极少发生；<strong>防篡改性</strong>正是建立在这之上。</li>
</ul>
<p>所以：<strong>传统系统</strong>攻破一个中心化服务器就能改数据；<strong>区块链</strong>要篡改历史，需要控制超过半数的共识资源，在成熟公链上这几乎不可行，因此链和智能合约具有<strong>强防篡改性</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八信任最小化从承诺到数学确定性">八、信任最小化：从承诺到数学确定性<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E5%85%AB%E4%BF%A1%E4%BB%BB%E6%9C%80%E5%B0%8F%E5%8C%96%E4%BB%8E%E6%89%BF%E8%AF%BA%E5%88%B0%E6%95%B0%E5%AD%A6%E7%A1%AE%E5%AE%9A%E6%80%A7" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>智能合约最根本的优势，是营造<strong>信任最小化</strong>的环境。传统体系里我们依赖<strong>交易对手</strong>：信保险公司会赔、信银行会保管资金、信雇主会发工资——依赖品牌、法律和承诺。</p>
<p>智能合约用<strong>代码的确定性</strong>替代这种依赖：不用信对方「会履约」，合约会按事先写好的逻辑<strong>确定性地执行</strong>。系统从「<strong>基于品牌/人</strong>」变成「<strong>基于数学和规则</strong>」——协议不再是口头的承诺，而是不可单方违背、可验证、自动执行的数字协议。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="九总结">九、总结<a href="https://blog.kuizuo.me/blog/advantages-of-smart-contracts#%E4%B9%9D%E6%80%BB%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<ul>
<li><strong>去中心化</strong>：无单点故障，没有一家能关掉合约。</li>
<li><strong>透明可验证</strong>：代码与操作公开，规则事先可知、不可暗中修改。</li>
<li><strong>伪匿名</strong>：地址不直接对应真人，兼顾可验证与隐私；高隐私需求可用 ZKP。</li>
<li><strong>效率</strong>：条件满足即自动执行，24/7，无需人工与中介。</li>
<li><strong>不可变性</strong>：部署后的代码不可改；<strong>可升级合约</strong>通过代理/实现分离事先设计升级能力，仍透明可审计。</li>
<li><strong>51% 攻击</strong>：理论上控制多数算力/权益可篡改历史；在成熟公链上成本极高、几乎不可行，链因此具备强防篡改性。</li>
<li><strong>信任最小化</strong>：从「信人」转向「信规则与代码」，协议可验证、自执行。</li>
</ul>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="区块链" term="区块链"/>
        <category label="智能合约" term="智能合约"/>
        <category label="DeFi" term="DeFi"/>
        <category label="Web3" term="Web3"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[GPT 是什么？一篇帮你真正搞懂 GPT 的通俗文章]]></title>
        <id>https://blog.kuizuo.me/blog/what-is-gpt</id>
        <link href="https://blog.kuizuo.me/blog/what-is-gpt"/>
        <updated>2026-01-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[用最直白、最形象的方式讲清 GPT 是什么、为什么强、为什么会出错；从「预测下一个词」到 Token、训练与幻觉，一文搞懂。]]></summary>
        <content type="html"><![CDATA[<p>这几年，GPT 这个词几乎到处都能看到。</p>
<p>有人拿它写文章，有人拿它写代码，有人用它做翻译、做总结、做聊天助手，甚至还有人把它当成“第二大脑”。</p>
<p>但很多人其实一直处在一种状态：</p>
<blockquote>
<p>好像天天都在用 GPT，
但真要说 GPT 到底是什么，却总感觉说不清。</p>
</blockquote>
<p>这篇文章的目标，就是帮你把这件事彻底讲明白。</p>
<p>你不需要有 AI 背景，也不需要懂数学。
我们只用最直白、最形象的方式，来真正理解：</p>
<p><strong>GPT 到底是什么，它为什么这么强，它又为什么会出错。</strong></p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一先用一句话理解-gpt">一、先用一句话理解 GPT<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E4%B8%80%E5%85%88%E7%94%A8%E4%B8%80%E5%8F%A5%E8%AF%9D%E7%90%86%E8%A7%A3-gpt" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果只能用一句话来解释 GPT，那就是：</p>
<blockquote>
<p><strong>GPT ​是一种通过学习海量文本，来预测“下一个最可能出现的词”的语言模型。</strong></p>
</blockquote>
<p>这句话听起来很技术，但你可以先抓住两个关键词：</p>
<ul>
<li><strong>学习了大量文字</strong></li>
<li><strong>预测下一个词</strong></li>
</ul>
<p>也就是说，GPT 表面上看起来像是在“理解你、思考你、回答你”，
但它底层最核心的动作其实是：</p>
<blockquote>
<p><strong>根据你前面输入的内容，预测接下来最合理、最可能出现的内容。</strong></p>
</blockquote>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二gpt-这个名字到底是什么意思">二、GPT 这个名字到底是什么意思？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E4%BA%8Cgpt-%E8%BF%99%E4%B8%AA%E5%90%8D%E5%AD%97%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E6%84%8F%E6%80%9D" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>GPT 其实是三个词的缩写：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-g--generative生成式">1. G = Generative（生成式）<a href="https://blog.kuizuo.me/blog/what-is-gpt#1-g--generative%E7%94%9F%E6%88%90%E5%BC%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>意思是：它会“生成内容”。</p>
<p>比如：</p>
<ul>
<li>生成一句回答</li>
<li>生成一段文章</li>
<li>生成一段代码</li>
<li>生成一个总结</li>
</ul>
<p>它不是简单从数据库里“查答案”，而是现算现生成。</p>
<p>这就像你问一个人问题，他不是翻书给你看，而是根据自己学过的东西，当场组织语言回答你。</p>
<hr>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-p--pre-trained预训练">2. P = Pre-trained（预训练）<a href="https://blog.kuizuo.me/blog/what-is-gpt#2-p--pre-trained%E9%A2%84%E8%AE%AD%E7%BB%83" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>意思是：它在正式使用之前，已经读过了海量文本。</p>
<p>这些文本可能包括：</p>
<ul>
<li>书籍</li>
<li>文章</li>
<li>网页</li>
<li>代码</li>
<li>问答内容</li>
<li>各种公开可用的语言数据</li>
</ul>
<p>你可以把它想象成：</p>
<blockquote>
<p>GPT 在“上岗”之前，已经做了很长时间的阅读训练。</p>
</blockquote>
<p>就像一个人先读了很多书，积累了大量语言经验，之后才能和你交流。</p>
<hr>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-t--transformer">3. T = Transformer<a href="https://blog.kuizuo.me/blog/what-is-gpt#3-t--transformer" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>这是 GPT 背后的核心模型架构。</p>
<p>这个词看起来最陌生，但你不用一上来就钻技术细节。
你先记住它最重要的一点：</p>
<blockquote>
<p><strong>Transformer ​的强大之处，在于它很擅长理解“上下文关系”。</strong></p>
</blockquote>
<p>也就是说，它不是一个字一个字孤立地看，而是会结合前后文来判断当前内容的意思。</p>
<p>比如你看到“苹果”这个词：</p>
<ul>
<li>在“我买了一个苹果”里，它可能是水果</li>
<li>在“苹果发布了新手机”里，它显然是公司</li>
</ul>
<p>Transformer 的厉害之处，就是它能根据上下文动态判断含义。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三gpt-本质上在干什么">三、GPT 本质上在干什么？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E4%B8%89gpt-%E6%9C%AC%E8%B4%A8%E4%B8%8A%E5%9C%A8%E5%B9%B2%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>很多人以为 GPT 是一个“知识库”，或者像搜索引擎一样“查资料”。</p>
<p>其实不是。</p>
<p>GPT 更像是一个：</p>
<blockquote>
<p><strong>超级强大的语言续写机器</strong></p>
</blockquote>
<p>比如你输入一句：</p>
<blockquote>
<p>今天天气很好，我准备去……</p>
</blockquote>
<p>GPT 会根据语言习惯继续往下接：</p>
<ul>
<li>散步</li>
<li>公园</li>
<li>晒太阳</li>
<li>跑步</li>
</ul>
<p>它会选择一个在上下文里最“像人会说出来”的延续。</p>
<p>所以你可以把 GPT 理解成这样一个系统：</p>
<blockquote>
<p><strong>它不是在“背答案”，而是在“根据上下文，生成最像正确答案的话”。</strong></p>
</blockquote>
<p>这也是为什么 GPT 既能回答问题，也能写故事、写代码、写邮件、做总结。</p>
<p>因为在它看来，这些事情虽然表面不同，但底层都是同一件事：</p>
<blockquote>
<p><strong>根据已有上下文，继续生成最合适的文本。</strong></p>
</blockquote>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四为什么预测下一个词这么简单的事会变得这么强">四、为什么“预测下一个词”这么简单的事，会变得这么强？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E5%9B%9B%E4%B8%BA%E4%BB%80%E4%B9%88%E9%A2%84%E6%B5%8B%E4%B8%8B%E4%B8%80%E4%B8%AA%E8%AF%8D%E8%BF%99%E4%B9%88%E7%AE%80%E5%8D%95%E7%9A%84%E4%BA%8B%E4%BC%9A%E5%8F%98%E5%BE%97%E8%BF%99%E4%B9%88%E5%BC%BA" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这恰恰是 GPT 最反直觉、也最有意思的地方。</p>
<p>很多人第一次听到 GPT 的原理时，会觉得：</p>
<blockquote>
<p>就这？
不就是猜下一个词吗？</p>
</blockquote>
<p>但问题在于：</p>
<p><strong>当这个“猜词”做得足够大、足够深、足够精细的时候，它就不再只是猜词了。</strong></p>
<p>它会逐渐表现出很多看起来像“理解”和“思考”的能力。</p>
<p>为什么？</p>
<p>因为人类语言本身就不是随便排列的。
语言背后藏着大量规律，比如：</p>
<ul>
<li>常识</li>
<li>逻辑</li>
<li>因果关系</li>
<li>语法结构</li>
<li>情绪表达</li>
<li>编程模式</li>
<li>知识关联</li>
</ul>
<p>如果一个模型真的学会了大量语言规律，那么它在预测下一个词的时候，就会被迫“顺便学会”很多东西。</p>
<p>你可以把它理解成：</p>
<blockquote>
<p><strong>为了把一句话接对，它必须理解这句话大概在说什么。</strong></p>
</blockquote>
<p>比如：</p>
<p>“如果今天下雨，我出门要带……”</p>
<p>GPT 大概率会接“伞”。</p>
<p>为什么不是“冰箱”？</p>
<p>因为它在大量文本中学到了“下雨”和“伞”之间的高关联关系。
这种关联看起来像常识，其实是在训练中一点点形成的。</p>
<p>所以你会看到一个神奇现象：</p>
<blockquote>
<p><strong>GPT ​训练目标很简单，但随着规模变大，它会涌现出复杂能力。</strong></p>
</blockquote>
<p>这就是为什么它能写代码、解释概念、总结文章、模拟角色、辅助思考。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五gpt-是怎么学会这些能力的">五、GPT 是怎么“学会”这些能力的？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E4%BA%94gpt-%E6%98%AF%E6%80%8E%E4%B9%88%E5%AD%A6%E4%BC%9A%E8%BF%99%E4%BA%9B%E8%83%BD%E5%8A%9B%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>可以把 GPT 的训练过程想象成一个“疯狂做完形填空的人”。</p>
<p>训练时，系统会给它看很多文本，然后故意遮住一部分，让它去猜：</p>
<ul>
<li>下一个词是什么</li>
<li>某个位置最合理的词是什么</li>
<li>在当前上下文里最自然的续写是什么</li>
</ul>
<p>它每猜一次，就会和标准答案比较：</p>
<ul>
<li>猜对了，继续强化</li>
<li>猜错了，就调整参数</li>
</ul>
<p>这个过程会重复很多很多次，多到人根本无法想象。</p>
<p>于是模型慢慢学会：</p>
<ul>
<li>句子怎么组成</li>
<li>段落怎么展开</li>
<li>论证怎么推进</li>
<li>代码怎么书写</li>
<li>故事怎么发展</li>
<li>问题通常怎么回答</li>
</ul>
<p>所以你可以把 GPT 的成长过程理解成：</p>
<blockquote>
<p><strong>它不是被人一条条教会知识，而是在海量文本中自己总结出语言规律。</strong></p>
</blockquote>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="六token-是什么为什么大家老在说它">六、Token 是什么？为什么大家老在说它？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E5%85%ADtoken-%E6%98%AF%E4%BB%80%E4%B9%88%E4%B8%BA%E4%BB%80%E4%B9%88%E5%A4%A7%E5%AE%B6%E8%80%81%E5%9C%A8%E8%AF%B4%E5%AE%83" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>理解 GPT，Token 是绕不过去的。</p>
<p>很多人以为 GPT 是按“字”或者“单词”来处理内容，其实更准确地说，它是按 <strong>Token</strong> 来处理的。</p>
<p>你可以把 Token 理解成：</p>
<blockquote>
<p><strong>模型眼里的“最小语言积木”</strong></p>
</blockquote>
<p>它不一定等于一个汉字，也不一定等于一个英文单词。
有时候一个字是一个 Token，有时候两个字是一个 Token，有时候一个长单词会被拆成几个 Token。</p>
<p>所以 GPT 的工作方式更像是在拼积木：</p>
<ul>
<li>先看前面一串 Token</li>
<li>再预测下一个最可能的 Token</li>
<li>然后一个接一个地生成下去</li>
</ul>
<p>于是，一段完整的话、文章、代码，其实都是它一步一步拼出来的。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="七gpt-为什么看起来像在思考">七、GPT 为什么看起来像在“思考”？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E4%B8%83gpt-%E4%B8%BA%E4%BB%80%E4%B9%88%E7%9C%8B%E8%B5%B7%E6%9D%A5%E5%83%8F%E5%9C%A8%E6%80%9D%E8%80%83" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这是很多人最容易产生错觉的地方。</p>
<p>因为 GPT 的回答常常非常流畅、很有条理，甚至还会分点、举例、做类比，所以我们很容易觉得：</p>
<blockquote>
<p>它是不是像人一样真的在思考？</p>
</blockquote>
<p>更准确地说，GPT 并不是人类那种意识层面的“思考”，但它确实会表现出一种很强的“推理样子”。</p>
<p>原因是：</p>
<blockquote>
<p><strong>它在训练中看过大量“人类如何表达思考过程”的文本。</strong></p>
</blockquote>
<p>比如：</p>
<ul>
<li>数学推导怎么写</li>
<li>分析文章怎么展开</li>
<li>论证如何一步一步推进</li>
<li>代码题如何解释思路</li>
</ul>
<p>所以它学会了“思考的语言形式”。</p>
<p>这就像一个演员演得非常像医生。
他不一定真的是医生，但他非常熟悉医生说话、判断和表达的方式。</p>
<p>当然，随着模型越来越强，它也确实具备了越来越强的推理能力。
但你还是要记住一个底层认知：</p>
<blockquote>
<p><strong>GPT ​的核心不是“拥有主观意识”，而是“极其擅长生成像思考结果一样的语言输出”。</strong></p>
</blockquote>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="八为什么说-gpt-像是在压缩互联网知识">八、为什么说 GPT 像是在“压缩互联网知识”？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E5%85%AB%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AF%B4-gpt-%E5%83%8F%E6%98%AF%E5%9C%A8%E5%8E%8B%E7%BC%A9%E4%BA%92%E8%81%94%E7%BD%91%E7%9F%A5%E8%AF%86" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这是一个非常形象、也非常有帮助的理解方式。</p>
<p>你可以想象一下，互联网上有海量文字：</p>
<ul>
<li>知识文章</li>
<li>论坛讨论</li>
<li>新闻内容</li>
<li>技术文档</li>
<li>教材</li>
<li>代码仓库</li>
<li>问答记录</li>
</ul>
<p>这些内容太多了，不可能原封不动塞进一个模型里当“数据库”。</p>
<p>GPT 做的事情更像是：</p>
<blockquote>
<p><strong>把这些海量文本中的语言规律、知识关联、表达模式，压缩进参数里。</strong></p>
</blockquote>
<p>注意，这个“压缩”不是像 ZIP 那样把原文完整存进去，
而是把里面的统计规律和结构模式提炼出来。</p>
<p>所以你可以把 GPT 想象成：</p>
<blockquote>
<p>它不是把整座图书馆背下来，
而是把图书馆里的“表达规律”和“知识分布”学进了脑子里。</p>
</blockquote>
<p>这也是为什么它有时候能说出很像某类知识的话，
但又不一定能精准复现某篇原文。</p>
<p>因为它记住的更多是“模式”，不是“逐字逐句的原文”。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="九gpt-是怎么生成回答的">九、GPT 是怎么生成回答的？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E4%B9%9Dgpt-%E6%98%AF%E6%80%8E%E4%B9%88%E7%94%9F%E6%88%90%E5%9B%9E%E7%AD%94%E7%9A%84" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>当你问 GPT 一个问题时，它大概会经历这样一个过程：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第一步把你的输入拆成-token">第一步：把你的输入拆成 Token<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E7%AC%AC%E4%B8%80%E6%AD%A5%E6%8A%8A%E4%BD%A0%E7%9A%84%E8%BE%93%E5%85%A5%E6%8B%86%E6%88%90-token" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>也就是把你的问题拆成模型能处理的语言单位。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第二步结合上下文理解你的意图">第二步：结合上下文理解你的意图<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E7%AC%AC%E4%BA%8C%E6%AD%A5%E7%BB%93%E5%90%88%E4%B8%8A%E4%B8%8B%E6%96%87%E7%90%86%E8%A7%A3%E4%BD%A0%E7%9A%84%E6%84%8F%E5%9B%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>它会参考你当前这句话，也会参考前面的对话内容。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第三步预测下一个最可能的-token">第三步：预测下一个最可能的 Token<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E7%AC%AC%E4%B8%89%E6%AD%A5%E9%A2%84%E6%B5%8B%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9C%80%E5%8F%AF%E8%83%BD%E7%9A%84-token" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>不是一次生成整段，而是先生成一个。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第四步继续预测下一个-token">第四步：继续预测下一个 Token<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E7%AC%AC%E5%9B%9B%E6%AD%A5%E7%BB%A7%E7%BB%AD%E9%A2%84%E6%B5%8B%E4%B8%8B%E4%B8%80%E4%B8%AA-token" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>一个接一个，一直生成下去。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="第五步直到形成完整回答">第五步：直到形成完整回答<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E7%AC%AC%E4%BA%94%E6%AD%A5%E7%9B%B4%E5%88%B0%E5%BD%A2%E6%88%90%E5%AE%8C%E6%95%B4%E5%9B%9E%E7%AD%94" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>于是你看到的那一大段自然语言，就出来了。</p>
<p>所以 GPT 写回答，不像人类“一口气想完整再说”，
更像是：</p>
<blockquote>
<p><strong>边预测、边续写、边形成完整内容。</strong></p>
</blockquote>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十gpt-为什么有时会胡说八道">十、GPT 为什么有时会“胡说八道”？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E5%8D%81gpt-%E4%B8%BA%E4%BB%80%E4%B9%88%E6%9C%89%E6%97%B6%E4%BC%9A%E8%83%A1%E8%AF%B4%E5%85%AB%E9%81%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这是理解 GPT 时非常重要的一点。</p>
<p>很多人第一次用 GPT，会被它惊艳。
但用着用着又会发现一个问题：</p>
<blockquote>
<p>它有时候明明说得很自信，结果却是错的。</p>
</blockquote>
<p>这就是常说的 ​**幻觉（Hallucination）**​。</p>
<p>为什么会这样？</p>
<p>因为 GPT 的目标并不是“保证事实绝对正确”，
而是：</p>
<blockquote>
<p><strong>生成一个在语言上最合理、最像答案的输出。</strong></p>
</blockquote>
<p>注意这两者差别很大：</p>
<ul>
<li><strong>最像正确答案</strong></li>
<li><strong>真正正确答案</strong></li>
</ul>
<p>不完全是一回事。</p>
<p>如果它对某个知识掌握不完整，
或者你的问题本身比较模糊，
或者它在现有上下文里没找到足够稳定的依据，</p>
<p>它仍然会尽量给你生成一个“听起来像那么回事”的回答。</p>
<p>于是就会出现一种情况：</p>
<blockquote>
<p>语言非常流畅，逻辑看起来也顺，但事实不一定对。</p>
</blockquote>
<p>这不是因为它故意骗你，
而是因为它本质上是一个“概率生成模型”，不是“真理引擎”。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十一那-gpt-和搜索引擎有什么区别">十一、那 GPT 和搜索引擎有什么区别？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E5%8D%81%E4%B8%80%E9%82%A3-gpt-%E5%92%8C%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>这个问题特别关键。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="搜索引擎更像什么">搜索引擎更像什么？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E6%9B%B4%E5%83%8F%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>搜索引擎更像图书馆管理员。</p>
<p>你问它问题，它去帮你找相关资料，然后把链接给你。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="gpt-更像什么">GPT 更像什么？<a href="https://blog.kuizuo.me/blog/what-is-gpt#gpt-%E6%9B%B4%E5%83%8F%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>GPT 更像一个读过很多书、能直接跟你对话的人。</p>
<p>你问它问题，它不只是给你资料，而是直接整合、组织、表达成答案。</p>
<p>所以两者的差别在于：</p>
<ul>
<li>搜索引擎偏“查找信息”</li>
<li>GPT 偏“生成答案”</li>
</ul>
<p>搜索擅长找来源，GPT 擅长组织表达。
搜索更接近“检索”，GPT 更接近“语言生成”。</p>
<p>这也是为什么最强的 AI 系统，往往不是只靠 GPT，
而是把 <strong>搜索 / ​检索能力 + ​GPT ​的生成能力</strong> 结合起来。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十二为什么-gpt-会让人感觉像会很多东西">十二、为什么 GPT 会让人感觉像“会很多东西”？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E5%8D%81%E4%BA%8C%E4%B8%BA%E4%BB%80%E4%B9%88-gpt-%E4%BC%9A%E8%AE%A9%E4%BA%BA%E6%84%9F%E8%A7%89%E5%83%8F%E4%BC%9A%E5%BE%88%E5%A4%9A%E4%B8%9C%E8%A5%BF" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>因为它学到的不是一个领域，而是跨领域的语言模式。</p>
<p>它在训练中接触过：</p>
<ul>
<li>历史</li>
<li>文学</li>
<li>科技</li>
<li>编程</li>
<li>商业</li>
<li>哲学</li>
<li>日常对话</li>
<li>学习资料</li>
</ul>
<p>于是你会感觉它像个“全能选手”。</p>
<p>但要注意：</p>
<blockquote>
<p><strong>它的“广”不代表每个领域都“深”。</strong></p>
</blockquote>
<p>它可能在很多话题上都能说两句，
但真正涉及高精度、强专业、强时效的问题时，仍然需要非常谨慎。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十三怎么用一句更生动的话记住-gpt">十三、怎么用一句更生动的话记住 GPT？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E5%8D%81%E4%B8%89%E6%80%8E%E4%B9%88%E7%94%A8%E4%B8%80%E5%8F%A5%E6%9B%B4%E7%94%9F%E5%8A%A8%E7%9A%84%E8%AF%9D%E8%AE%B0%E4%BD%8F-gpt" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>如果你想把 GPT 讲给别人听，我建议你记住这个比喻：</p>
<blockquote>
<p><strong>GPT ​像一个读过海量文字、极其会说话、特别擅长接话的超级语言大脑。</strong></p>
</blockquote>
<p>它不是在数据库里翻答案，
而是在你给出的上下文基础上，
结合它训练中学到的无数语言规律，
一步一步生成最可能、最像人写出来的内容。</p>
<p>再简化一点，也可以说：</p>
<blockquote>
<p><strong>GPT ​的本质，就是“通过学习海量文本，获得强大续写能力的生成式语言模型”。</strong></p>
</blockquote>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="十四最后做个总结你到底应该怎样理解-gpt">十四、最后做个总结：你到底应该怎样理解 GPT？<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E5%8D%81%E5%9B%9B%E6%9C%80%E5%90%8E%E5%81%9A%E4%B8%AA%E6%80%BB%E7%BB%93%E4%BD%A0%E5%88%B0%E5%BA%95%E5%BA%94%E8%AF%A5%E6%80%8E%E6%A0%B7%E7%90%86%E8%A7%A3-gpt" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>把这篇文章读完之后，你可以把 GPT 归纳为下面这几句话：</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-gpt-是一种生成式预训练语言模型">1. GPT 是一种生成式预训练语言模型<a href="https://blog.kuizuo.me/blog/what-is-gpt#1-gpt-%E6%98%AF%E4%B8%80%E7%A7%8D%E7%94%9F%E6%88%90%E5%BC%8F%E9%A2%84%E8%AE%AD%E7%BB%83%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>它提前读了很多文本，学会了语言规律，然后能生成回答。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-它的核心机制是预测下一个-token">2. 它的核心机制是预测下一个 Token<a href="https://blog.kuizuo.me/blog/what-is-gpt#2-%E5%AE%83%E7%9A%84%E6%A0%B8%E5%BF%83%E6%9C%BA%E5%88%B6%E6%98%AF%E9%A2%84%E6%B5%8B%E4%B8%8B%E4%B8%80%E4%B8%AA-token" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>表面上像聊天、写作、推理，底层其实都是在做语言续写。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-它之所以强是因为语言本身包含了知识与逻辑">3. 它之所以强，是因为语言本身包含了知识与逻辑<a href="https://blog.kuizuo.me/blog/what-is-gpt#3-%E5%AE%83%E4%B9%8B%E6%89%80%E4%BB%A5%E5%BC%BA%E6%98%AF%E5%9B%A0%E4%B8%BA%E8%AF%AD%E8%A8%80%E6%9C%AC%E8%BA%AB%E5%8C%85%E5%90%AB%E4%BA%86%E7%9F%A5%E8%AF%86%E4%B8%8E%E9%80%BB%E8%BE%91" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>当模型学会语言规律时，也会顺带学到很多知识结构。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-它不是搜索引擎也不是绝对正确的知识库">4. 它不是搜索引擎，也不是绝对正确的知识库<a href="https://blog.kuizuo.me/blog/what-is-gpt#4-%E5%AE%83%E4%B8%8D%E6%98%AF%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E4%B9%9F%E4%B8%8D%E6%98%AF%E7%BB%9D%E5%AF%B9%E6%AD%A3%E7%A1%AE%E7%9A%84%E7%9F%A5%E8%AF%86%E5%BA%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>它擅长生成“像答案的内容”，但不保证永远真实准确。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-它最像一个超级会表达的语言大脑">5. 它最像一个“超级会表达的语言大脑”<a href="https://blog.kuizuo.me/blog/what-is-gpt#5-%E5%AE%83%E6%9C%80%E5%83%8F%E4%B8%80%E4%B8%AA%E8%B6%85%E7%BA%A7%E4%BC%9A%E8%A1%A8%E8%BE%BE%E7%9A%84%E8%AF%AD%E8%A8%80%E5%A4%A7%E8%84%91" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<p>你给它上下文，它就接着往下生成。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="结尾">结尾<a href="https://blog.kuizuo.me/blog/what-is-gpt#%E7%BB%93%E5%B0%BE" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>所以，GPT 最迷人的地方其实就在这里：</p>
<blockquote>
<p>它做的事情看起来很简单，只是在预测下一个词；
但当这个能力被放大到极致时，它竟然表现出了接近“理解、推理、创作、对话”的综合能力。</p>
</blockquote>
<p>这也是为什么 GPT 会成为这一代 AI 浪潮的核心。</p>
<p>因为它让我们第一次如此直观地看到：</p>
<blockquote>
<p><strong>只要把语言学得足够深，机器就能表现出非常惊人的智能。</strong></p>
</blockquote>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="AI" term="AI"/>
        <category label="GPT" term="GPT"/>
        <category label="大模型" term="大模型"/>
        <category label="ChatGPT" term="ChatGPT"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[NestJS AOP 架构]]></title>
        <id>https://blog.kuizuo.me/blog/nestjs-aop</id>
        <link href="https://blog.kuizuo.me/blog/nestjs-aop"/>
        <updated>2026-01-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[NestJS 里 AOP 是什么？请求链路中 Middleware、Guard、Interceptor、Pipe、ExceptionFilter 的执行顺序、职责与使用方式。]]></summary>
        <content type="html"><![CDATA[<p><strong>AOP（面向切面编程）</strong> 把与业务无关的通用逻辑（日志、鉴权、参数校验等）从业务代码里抽出来，在请求的固定阶段统一执行。NestJS 的 <strong>Middleware、Guard、Interceptor、Pipe、ExceptionFilter</strong> 就是这套思想的具体实现。下面先说明 AOP 是什么，再给出请求链路顺序、各组件的职责与用法，最后用表格做速查。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="一aop-是什么">一、AOP 是什么？<a href="https://blog.kuizuo.me/blog/nestjs-aop#%E4%B8%80aop-%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p><strong>AOP（Aspect-Oriented Programming，面向切面编程）</strong> 是一种编程思想：把与「业务逻辑」无关的通用逻辑（如日志、鉴权、计时、参数校验、异常格式化）从业务代码里抽出来，单独写成「<strong>切面</strong>」，在请求的某个阶段统一执行。</p>
<ul>
<li><strong>好处</strong>：业务代码只关心「做什么」，通用逻辑集中维护、可复用、易测试。</li>
<li><strong>在 NestJS 里</strong>：Middleware、Guard、Interceptor、Pipe、ExceptionFilter 都是 AOP 的体现，它们在不同阶段介入请求，各管一类横切关注点。</li>
</ul>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="二请求链路骨架执行顺序">二、请求链路骨架（执行顺序）<a href="https://blog.kuizuo.me/blog/nestjs-aop#%E4%BA%8C%E8%AF%B7%E6%B1%82%E9%93%BE%E8%B7%AF%E9%AA%A8%E6%9E%B6%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<p>一个请求从进来到返回，大致经过下面的顺序（由外到内再往外）：</p>
<div class="language-javascript codeBlockContainer_APcc theme-code-block" style="--prism-background-color:hsl(230, 1%, 98%);--prism-color:hsl(230, 8%, 24%)"><div class="codeBlockContent_m3Ux"><pre class="prism-code language-javascript codeBlock_qGQc thin-scrollbar" style="background-color:hsl(230, 1%, 98%);color:hsl(230, 8%, 24%)"><code class="codeBlockLines_p187"><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">请求进入</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">① </span><span class="token maybe-class-name">Middleware（中间件）</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">② </span><span class="token maybe-class-name">Guard（守卫）</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">③ </span><span class="token maybe-class-name">Interceptor（拦截器）before</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">④ </span><span class="token maybe-class-name">Pipe（管道）—</span><span class="token plain"> 校验</span><span class="token operator" style="color:hsl(221, 87%, 60%)">/</span><span class="token plain">转换参数</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">⑤ </span><span class="token maybe-class-name">Controller</span><span class="token plain"> 方法（业务逻辑）</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">⑥ </span><span class="token maybe-class-name">Interceptor（拦截器）after</span><span class="token plain"> </span><span class="token operator" style="color:hsl(221, 87%, 60%)">/</span><span class="token plain"> 响应转换</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">⑦ 若抛异常 → </span><span class="token maybe-class-name">ExceptionFilter（异常过滤器）</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:hsl(230, 8%, 24%)"><span class="token plain">响应返回</span><br></span></code></pre><div class="buttonGroup_6DOT"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_FhaS" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_phi_"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_FfTR"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>可以简单记：<strong>Middleware → Guard → Interceptor → Pipe → 业务 → Interceptor 收尾 →（有异常则）ExceptionFilter</strong>。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="三各组件的职责与使用方式">三、各组件的职责与使用方式<a href="https://blog.kuizuo.me/blog/nestjs-aop#%E4%B8%89%E5%90%84%E7%BB%84%E4%BB%B6%E7%9A%84%E8%81%8C%E8%B4%A3%E4%B8%8E%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="1-middleware中间件">1. Middleware（中间件）<a href="https://blog.kuizuo.me/blog/nestjs-aop#1-middleware%E4%B8%AD%E9%97%B4%E4%BB%B6" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>是什么</strong>：在「进入路由之前」对请求/响应做处理的函数，可访问 <code>req</code>、<code>res</code>、<code>next</code>。</li>
<li><strong>执行时机</strong>：最早，在 Guard 之前；且是 Node/Express 层概念，不区分具体是哪个 Controller 或哪个 Handler。</li>
<li><strong>如何应用</strong>：<!-- -->
<ul>
<li>在某个模块里实现 <code>NestModule</code>，在 <code>configure(consumer: MiddlewareConsumer)</code> 里用 <code>consumer.apply(LogMiddleware).forRoutes('aaa*')</code> 绑定到满足条件的路由。</li>
<li>不能直接注入 Nest 的 Provider（需通过 <code>req</code> 或把 Middleware 包在带 Module 的模块里用 <code>Inject</code> 等方式间接用）。</li>
</ul>
</li>
<li><strong>适用场景</strong>：日志、请求 ID、CORS、body 解析、和「路径/前缀」相关的通用预处理（例如给 <code>/api/*</code> 打日志）。</li>
</ul>
<p><strong>项目中的用法</strong>：对 <code>aaa*</code> 路由打 before2 / after2 日志。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="2-guard守卫">2. Guard（守卫）<a href="https://blog.kuizuo.me/blog/nestjs-aop#2-guard%E5%AE%88%E5%8D%AB" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>是什么</strong>：根据当前请求的上下文（如用户、角色、Token）判断「是否允许继续」；返回 <code>true</code> 放行，<code>false</code> 抛 403。</li>
<li><strong>执行时机</strong>：在 Middleware 之后、Interceptor 和 Pipe 之前；若 <code>canActivate</code> 为 <code>false</code>，后续的 Interceptor、Pipe、Controller 都不会执行。</li>
<li><strong>如何应用</strong>：<!-- -->
<ul>
<li><strong>全局</strong>：在 AppModule 的 <code>providers</code> 里 <code>{ provide: APP_GUARD, useClass: LoginGuard }</code>。</li>
<li><strong>控制器/方法级</strong>：在 Controller 或方法上 <code>@UseGuards(LoginGuard)</code>。</li>
</ul>
</li>
<li><strong>适用场景</strong>：登录态校验、权限/角色校验、接口是否对外开放等。</li>
</ul>
<p><strong>项目中的用法</strong>：LoginGuard 在 aaa 接口上做登录检查（当前写死 <code>return false</code> 会拦掉请求）。</p>
<p><strong>全局 Guard 的两种注册方式</strong></p>
<p>全局 Guard 可以有两种写法，区别在于「谁创建实例、能不能用依赖注入」：</p>
<table><thead><tr><th>对比项</th><th>方式 1：<code>app.useGlobalGuards(new LoginGuard())</code>（main.ts）</th><th>方式 2：<code>APP_GUARD</code> + <code>useClass: LoginGuard</code>（app.module.ts）</th></tr></thead><tbody><tr><td>创建方式</td><td>自己在 <code>main</code> 里 <code>new LoginGuard()</code>，<strong>不经过 Nest 的 DI 容器</strong></td><td>由 Nest 按 provider 创建实例，<strong>走完整 DI 流程</strong></td></tr><tr><td>依赖注入</td><td>Guard 构造函数里<strong>不能</strong>通过 <code>@Injectable()</code> 注入其他服务，否则要自己传或拿不到</td><td>可以在 LoginGuard 构造函数里<strong>注入任意 provider</strong>（如 ConfigService、其他 Service），Nest 会自动注入</td></tr><tr><td>适用场景</td><td>Guard 无依赖、或只做简单判断时可以用</td><td>Guard 需要依赖其他服务、配置、数据库等时，用这种方式</td></tr><tr><td>注册时机</td><td>在 <code>bootstrap()</code> 里、应用创建之后立刻注册</td><td>在模块初始化时注册，和别的 provider 一起解析依赖</td></tr></tbody></table>
<p><strong>对比小结</strong>：需要依赖注入（例如 Guard 里要用别的 Service）→ 用 <strong>APP_GUARD + useClass</strong>。无依赖、简单逻辑 → 两种都行，但 APP_GUARD 更符合 Nest 的写法，也方便以后在 Guard 里加依赖。<strong>若两种都写，同一个 Guard 会注册两次，请求时就会跑两遍</strong>，所以只保留一种即可。</p>
<p><strong>通俗理解：providers 和 DI 的关系</strong></p>
<p>你理解得对，可以概括成两句话：</p>
<ul>
<li><strong>凡是在 <code>providers</code> 里注册的类（包括 <code>APP_GUARD</code>、<code>APP_PIPE</code> 等用的 <code>useClass: XxxGuard</code>），都会由 Nest 的 DI 容器来创建实例</strong>，创建时会解析构造函数里的依赖，自动注入该模块能拿到的其他 provider。</li>
<li><strong>「能拿到」的前提是：要么在同一个模块的 <code>providers</code> 里，要么在其它模块的 <code>exports</code> 里且当前模块已经 <code>imports</code> 了那个模块</strong>。所以只要模块之间通过 <code>imports</code> / <code>exports</code> 建立好关系，Guard、Pipe、Interceptor 等都可以在构造函数里注入任意「已导出」的服务，互相引用。</li>
</ul>
<p>因此：用 <strong>APP_GUARD + useClass</strong> 注册的 Guard，和普通 Service 一样会进 DI，可以放心在 Guard 里写 <code>constructor(private config: ConfigService)</code> 等，Nest 会自动注入；用 <code>main.ts</code> 里 <code>useGlobalGuards(new LoginGuard())</code> 时，Guard 是自己在外面 new 的，不经过 DI，就没办法自动注入其他服务。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="3-interceptor拦截器">3. Interceptor（拦截器）<a href="https://blog.kuizuo.me/blog/nestjs-aop#3-interceptor%E6%8B%A6%E6%88%AA%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>是什么</strong>：在「调用 Controller 方法前后」插入逻辑，可修改请求/响应、记录耗时、统一包装返回结构等；基于 RxJS，可写 <code>next.handle().pipe(...)</code>。</li>
<li><strong>执行时机</strong>：Guard 之后、Pipe 和 Controller 之前执行「before」；Controller 返回后、在响应发出前执行「after」。</li>
<li><strong>如何应用</strong>：<!-- -->
<ul>
<li><strong>全局</strong>：<code>{ provide: APP_INTERCEPTOR, useClass: TimeInterceptor }</code>。</li>
<li><strong>控制器/方法级</strong>：<code>@UseInterceptors(TimeInterceptor)</code>。</li>
</ul>
</li>
<li><strong>适用场景</strong>：接口耗时统计、统一响应格式（如 <code>{ code, data, message }</code>）、缓存、日志、超时处理。</li>
</ul>
<p><strong>项目中的用法</strong>：TimeInterceptor 在 <code>next.handle()</code> 前后计时并打印。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="4-pipe管道">4. Pipe（管道）<a href="https://blog.kuizuo.me/blog/nestjs-aop#4-pipe%E7%AE%A1%E9%81%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>是什么</strong>：只负责「对进入 Controller 的某个参数」做转换或校验；输入原始值，输出转换后的值，校验失败可抛异常（由 ExceptionFilter 处理）。</li>
<li><strong>执行时机</strong>：在 Interceptor 的「before」之后、真正执行 Controller 方法之前；只针对绑定了该 Pipe 的参数（或路由）。</li>
<li><strong>如何应用</strong>：<!-- -->
<ul>
<li><strong>全局</strong>：<code>{ provide: APP_PIPE, useClass: ValidatePipe }</code>，会对所有满足 Pipe 使用条件的参数生效。</li>
<li><strong>控制器级</strong>：<code>@UsePipes(ValidatePipe)</code>，对该 Controller 下所有参数生效。</li>
<li><strong>方法/参数级</strong>：<code>@Query('num', ValidatePipe) num: number</code>，只对该参数生效。</li>
</ul>
</li>
<li><strong>适用场景</strong>：参数类型转换（string→number）、校验（格式、范围）、默认值、清洗输入。</li>
</ul>
<p><strong>项目中的用法</strong>：ValidatePipe 对 <code>num</code> 做数字校验与转换（×10），非法时抛 <code>BadRequestException</code>。</p>
<h3 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="5-exceptionfilter异常过滤器">5. ExceptionFilter（异常过滤器）<a href="https://blog.kuizuo.me/blog/nestjs-aop#5-exceptionfilter%E5%BC%82%E5%B8%B8%E8%BF%87%E6%BB%A4%E5%99%A8" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h3>
<ul>
<li><strong>是什么</strong>：捕获在 Guard、Interceptor、Pipe、Controller 中抛出的异常，转换成对客户端的 HTTP 响应（状态码、body 格式）。</li>
<li><strong>执行时机</strong>：不占「正常顺序」，只在有异常抛出时触发；可指定只捕获某类异常（如 <code>@Catch(BadRequestException)</code>）。</li>
<li><strong>如何应用</strong>：<!-- -->
<ul>
<li><strong>全局</strong>：<code>{ provide: APP_FILTER, useClass: TestFilter }</code>。</li>
<li><strong>控制器/方法级</strong>：<code>@UseFilters(TestFilter)</code>。</li>
</ul>
</li>
<li><strong>适用场景</strong>：统一错误码、错误文案、日志上报、区分开发/生产环境的不同错误格式。</li>
</ul>
<p><strong>项目中的用法</strong>：TestFilter 只捕获 <code>BadRequestException</code>，返回 400 和 <code>test: ${message}</code> 的 JSON。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="四应用方式小结">四、应用方式小结<a href="https://blog.kuizuo.me/blog/nestjs-aop#%E5%9B%9B%E5%BA%94%E7%94%A8%E6%96%B9%E5%BC%8F%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<table><thead><tr><th>组件</th><th>全局注册方式</th><th>控制器/方法级</th></tr></thead><tbody><tr><td>Middleware</td><td>无「全局」token，需在模块 <code>configure</code> 里按路由绑定</td><td><code>consumer.apply(Middleware).forRoutes(...)</code></td></tr><tr><td>Guard</td><td><code>APP_GUARD</code></td><td><code>@UseGuards(Guard)</code></td></tr><tr><td>Interceptor</td><td><code>APP_INTERCEPTOR</code></td><td><code>@UseInterceptors(Interceptor)</code></td></tr><tr><td>Pipe</td><td><code>APP_PIPE</code></td><td><code>@UsePipes(Pipe)</code>、<code>@Param/Query/Body('key', Pipe)</code></td></tr><tr><td>ExceptionFilter</td><td><code>APP_FILTER</code></td><td><code>@UseFilters(Filter)</code></td></tr></tbody></table>
<p>全局注册在 <strong>AppModule</strong> 的 <code>providers</code> 里写即可；控制器/方法级用装饰器，粒度更细。</p>
<hr>
<h2 class="anchor anchorWithHideOnScrollNavbar_j5ym" id="五场景速查">五、场景速查<a href="https://blog.kuizuo.me/blog/nestjs-aop#%E4%BA%94%E5%9C%BA%E6%99%AF%E9%80%9F%E6%9F%A5" class="hash-link" aria-label="标题的直接链接" title="标题的直接链接">​</a></h2>
<table><thead><tr><th>需求</th><th>更合适的组件</th></tr></thead><tbody><tr><td>按路径/前缀做日志、限流、请求 ID</td><td>Middleware</td></tr><tr><td>登录/权限判断，不放行就 403</td><td>Guard</td></tr><tr><td>统一响应格式、耗时、缓存、日志</td><td>Interceptor</td></tr><tr><td>参数校验、类型转换、默认值</td><td>Pipe</td></tr><tr><td>统一错误响应格式、错误码、错误日志</td><td>ExceptionFilter</td></tr></tbody></table>]]></content>
        <author>
            <name>Jimmy</name>
            <email>hijimmypeng@gmail.com</email>
            <uri>https://github.com/pen-duo</uri>
        </author>
        <category label="NestJS" term="NestJS"/>
        <category label="TypeScript" term="TypeScript"/>
        <category label="Node.js" term="Node.js"/>
        <category label="后端" term="后端"/>
    </entry>
</feed>