<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>程沛权 - 养了三只猫</title>
        <link>https://chengpeiquan.com/</link>
        <description>一个养了三只猫的花臂男。</description>
        <lastBuildDate>Sat, 09 May 2026 03:05:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>程沛权 - 养了三只猫</title>
            <url>https://chengpeiquan.com/avatar-256x256.png</url>
            <link>https://chengpeiquan.com/</link>
        </image>
        <copyright>© 2014-PRESENT 程沛权</copyright>
        <atom:link href="https://chengpeiquan.com/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[适用于 Oxlint 与 Oxfmt 的 Oxc 优先工作流]]></title>
            <link>https://chengpeiquan.com/article/an-oxc-first-workflow-package-for-oxlint-and-oxfmt</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/an-oxc-first-workflow-package-for-oxlint-and-oxfmt</guid>
            <pubDate>Mon, 13 Apr 2026 17:30:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>很早之前就对 <a href="https://oxc.rs/">Oxc</a> 这个项目一直保持关注，看到前段时间开始进入 1.x 版本，一直想弄一套配置，这几天终于有时间搞一下。</p>
<p>不过目前阶段 OXLint 还是无法完整替代 ESLint ，只能是以 OXLint 为主， ESLint 为辅，但是两者可以完美结合，对项目来说，仍然可以以一套 Lint 方案去解决代码问题， OXFmt 倒是可以完美代替 Prettier 原生规则，只是部分插件仍然需要取舍，例如 Markdown 的盘古之白格式化，我只能暂时舍弃。</p>
<p>由于需要两套 Lint 同时工作，所以这个配置包还没有正式定名为 oxc-config ，暂时只称之为 Integration ，一个集成方案或者说是一套工作流。</p>
<p>这是我在 GitHub 上发布的一个开源项目。如果它对你有帮助，欢迎前往 <a href="https://github.com/chengpeiquan/bassist">bassist</a> 点一个 Star。</p>
<p><code>@bassist/oxc-integration</code> 是一个 Oxc 优先的工作流包，适合希望现在就用上带类型提示的 <code>oxlint.config.ts</code> 与 <code>oxfmt.config.ts</code>，同时在 Oxc 能力尚未完全覆盖时保留 ESLint 兜底能力的项目。</p>
<h2 id="使用方法"><a tabindex="-1" href="#使用方法"><span></span></a>使用方法</h2>
<p>通常只需要四步：</p>
<ol>
<li>安装依赖（参考：<a href="#%E5%AE%89%E8%A3%85">安装</a>）</li>
<li>添加 <code>oxlint.config.ts</code>（参考：<a href="#oxlint-%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B">Oxlint 快速开始</a>）</li>
<li>添加 <code>oxfmt.config.ts</code>（参考：<a href="#oxfmt-%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B">Oxfmt 快速开始</a>）</li>
<li>只有在项目需要 fallback coverage 时，再补 <code>eslint.config.js</code>（参考：<a href="#eslint-fallback-%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B">ESLint Fallback 快速开始</a>）</li>
</ol>
<h2 id="安装"><a tabindex="-1" href="#安装"><span></span></a>安装</h2>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-sh"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">npm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> i</span><span style="color:#986801;--shiki-dark:#569CD6"> -D</span><span style="color:#50A14F;--shiki-dark:#CE9178"> oxlint</span><span style="color:#50A14F;--shiki-dark:#CE9178"> oxfmt</span><span style="color:#50A14F;--shiki-dark:#CE9178"> @bassist/oxc-integration</span></span></code></pre>
<p>如果你的项目需要 ESLint fallback，请再安装对应框架所需的 ESLint 生态依赖。</p>
<h2 id="编辑器设置"><a tabindex="-1" href="#编辑器设置"><span></span></a>编辑器设置</h2>
<p><code>oxlint</code> / <code>eslint</code> 的 CLI 检查结果，与编辑器里的实时诊断并不是同一件事。</p>
<p>如果你希望在编码过程中就获得 Oxc 的实时 lint 提示，还需要额外安装对应的编辑器插件。否则就可能出现“执行 lint 命令时能查出错误，但编辑器里没有任何
提示”的情况。</p>
<ul>
<li>VS Code / Cursor：安装官方 <code>oxc.oxc-vscode</code> 扩展</li>
<li>其他编辑器：参考 Oxc 官方的编辑器接入文档</li>
</ul>
<p>官方文档： <a href="https://oxc.rs/docs/guide/usage/linter/editors.html">https://oxc.rs/docs/guide/usage/linter/editors.html</a></p>
<p>Oxc 的编辑器扩展会通过项目本地的 <code>oxlint --lsp</code> 工作，因此也请确保项目的
<code>devDependencies</code> 里已经安装了 <code>oxlint</code>。</p>
<p>如果你使用 VS Code 或 Cursor，也可以在项目里补一个
<code>.vscode/settings.json</code>，让保存时修复和类型感知检查更完整：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "editor.codeActionsOnSave"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "source.fixAll.eslint"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"always"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "source.fixAll.oxc"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"always"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  },</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "oxc.typeAware"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#0184BC;--shiki-dark:#569CD6">true</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<ul>
<li><code>source.fixAll.oxc</code> 用于通过 <code>oxc.oxc-vscode</code> 扩展启用 Oxc 的保存时自动修复</li>
<li><code>oxc.typeAware: true</code> 可以在 TypeScript 项目里提供类型感知诊断</li>
<li>如果你的项目还保留 <code>eslint.config.js</code> 作为 fallback layer，建议继续保留 <code>source.fixAll.eslint</code></li>
<li>如果项目已经是纯 Oxlint 工作流，可以移除 ESLint 的保存时动作</li>
</ul>
<h2 id="定位"><a tabindex="-1" href="#定位"><span></span></a>定位</h2>
<ul>
<li>如果你需要当前稳定、完整的 ESLint 优先方案，请使用 <code>@bassist/eslint-config</code></li>
<li>如果你想采用 Oxc 优先的工作流，请使用 <code>@bassist/oxc-integration</code></li>
<li>可以把它理解为未来 <code>@bassist/oxc-config</code> 的过渡形态</li>
</ul>
<h2 id="工作方式"><a tabindex="-1" href="#工作方式"><span></span></a>工作方式</h2>
<p><code>@bassist/oxc-integration</code> 采用 Oxc-first workflow：</p>
<ul>
<li><code>oxlint</code> 是主 Linter</li>
<li><code>eslint</code> 是兜底 Linter，用来补齐 Oxc 还没完全覆盖的规则</li>
<li><code>oxfmt</code> 是主 Formatter</li>
</ul>
<p>这也是为什么某些项目现在仍然会同时保留 <code>oxlint.config.ts</code> 和 <code>eslint.config.js</code>。
它们不再是两个并列的主配置文件，而是：</p>
<ul>
<li><code>oxlint.config.ts</code>：主 Lint 配置</li>
<li><code>eslint.config.js</code>：兜底 Lint 配置</li>
</ul>
<p>在这些 fallback presets 内部，会自动接入 <code>eslint-plugin-oxlint</code>，用于关闭那些已经由 Oxlint 覆盖的 ESLint 重叠规则。</p>
<h2 id="什么时候需要-eslint-fallback"><a tabindex="-1" href="#什么时候需要-eslint-fallback"><span></span></a>什么时候需要 ESLint Fallback</h2>
<p>如果 Oxc 已经足够覆盖你的项目，可以只使用 <code>oxlint.config.ts</code>。
如果项目仍然依赖框架或生态特定的 ESLint 能力，则再补一个 <code>eslint.config.js</code>。</p>
<p>通常可以这样理解：</p>
<ul>
<li><code>base</code> / <code>node</code>：很多场景只用 Oxlint 就够了</li>
<li><code>react</code>：大多数情况下可以 Oxc 优先，但很多项目仍建议保留 ESLint fallback</li>
<li><code>vue</code>：默认建议保留 ESLint fallback</li>
<li><code>next</code>：默认建议保留 ESLint fallback</li>
<li><code>tailwindcss</code>：优先使用 <code>oxlintPresets.tailwindcss()</code> 通过 Oxc JS plugin lint；如果项目仍需要 ESLint 专属覆盖，再补 ESLint fallback</li>
<li><code>vitest</code>：如果你需要更完整的测试规则，建议保留 ESLint fallback</li>
</ul>
<h2 id="内建的-fallback-coverage"><a tabindex="-1" href="#内建的-fallback-coverage"><span></span></a>内建的 Fallback Coverage</h2>
<p>当前 <code>@bassist/oxc-integration</code> 已内建这些 ESLint fallback presets：</p>
<ul>
<li><code>javascript</code></li>
<li><code>typescript</code></li>
<li><code>jsx</code></li>
<li><code>imports</code></li>
<li><code>jsonc</code></li>
<li><code>markdown</code></li>
<li><code>react</code></li>
<li><code>tailwindcss</code></li>
<li><code>vue</code></li>
<li><code>next</code></li>
<li><code>vitest</code></li>
</ul>
<p>当前暂不纳入这个 Oxc-first 工作流范围的内容：</p>
<ul>
<li><code>lint-md</code></li>
<li>基于 Prettier 的 Markdown 内容工作流</li>
</ul>
<p>这些能力后续可以单独作为内容层工作流再设计。</p>
<h2 id="配置总览"><a tabindex="-1" href="#配置总览"><span></span></a>配置总览</h2>
<table>
<thead>
<tr>
<th>项目类型</th>
<th><code>oxlint.config.ts</code></th>
<th><code>eslint.config.js</code></th>
<th><code>oxfmt.config.ts</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>Base TS/JS</td>
<td>必需</td>
<td>可选</td>
<td>必需</td>
</tr>
<tr>
<td>Node</td>
<td>必需</td>
<td>可选</td>
<td>必需</td>
</tr>
<tr>
<td>React</td>
<td>必需</td>
<td>推荐</td>
<td>必需</td>
</tr>
<tr>
<td>Vue</td>
<td>必需</td>
<td>推荐</td>
<td>必需</td>
</tr>
<tr>
<td>Next</td>
<td>必需</td>
<td>推荐</td>
<td>必需</td>
</tr>
</tbody>
</table>
<h2 id="oxlint-快速开始"><a tabindex="-1" href="#oxlint-快速开始"><span></span></a>Oxlint 快速开始</h2>
<h3 id="base-tsjs"><a tabindex="-1" href="#base-tsjs"><span></span></a>Base TS/JS</h3>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// oxlint.config.ts</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/oxc-integration'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">base</span><span style="color:#383A42;--shiki-dark:#D4D4D4">())</span></span></code></pre>
<h3 id="react"><a tabindex="-1" href="#react"><span></span></a>React</h3>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// oxlint.config.ts</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/oxc-integration'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">react</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(), </span><span style="color:#383A42;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">vitest</span><span style="color:#383A42;--shiki-dark:#D4D4D4">())</span></span></code></pre>
<h3 id="vue"><a tabindex="-1" href="#vue"><span></span></a>Vue</h3>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// oxlint.config.ts</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/oxc-integration'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">vue</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(), </span><span style="color:#383A42;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">vitest</span><span style="color:#383A42;--shiki-dark:#D4D4D4">())</span></span></code></pre>
<p>如果你需要官方 <code>oxlint</code> 类型，请直接从 <code>oxlint</code> 包导入，而不是从本包导入。</p>
<h2 id="oxlint-内建默认值"><a tabindex="-1" href="#oxlint-内建默认值"><span></span></a>Oxlint 内建默认值</h2>
<p><code>oxlintPresets.base()</code> 并不是空壳，它已经内置了这个包约定的 Oxc 基线配置。</p>
<p>当前内建默认值包括：</p>
<ul>
<li>plugins：<code>typescript</code>、<code>oxc</code></li>
<li>categories：
<ul>
<li><code>correctness: error</code></li>
<li><code>suspicious: error</code></li>
<li><code>pedantic: warn</code></li>
<li><code>style: off</code></li>
</ul>
</li>
<li>基础规则覆盖：
<ul>
<li><code>typescript/no-explicit-any: off</code></li>
</ul>
</li>
</ul>
<p>然后更高层的 presets 会在这份基线上继续扩展：</p>
<ul>
<li><code>oxlintPresets.node()</code> 会补充 Node 运行时环境</li>
<li><code>oxlintPresets.react()</code> 会补充 <code>react</code>、<code>react-perf</code>、<code>jsx-a11y</code></li>
<li><code>oxlintPresets.tailwindcss()</code> 会通过 Oxlint JS plugins 加载 <code>eslint-plugin-better-tailwindcss</code></li>
<li><code>oxlintPresets.vue()</code> 会补充 <code>vue</code></li>
<li><code>oxlintPresets.vitest()</code> 会补充 <code>vitest</code></li>
</ul>
<h2 id="如何扩展-oxlint-presets"><a tabindex="-1" href="#如何扩展-oxlint-presets"><span></span></a>如何扩展 Oxlint Presets</h2>
<p>建议先使用内建默认值，再把项目特有的差异作为额外配置传入。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// oxlint.config.ts</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/oxc-integration'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">react</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(), {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  rules</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#CE9178">    'no-console'</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'warn'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  },</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  ignorePatterns</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#50A14F;--shiki-dark:#CE9178">'fixtures/**'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">})</span></span></code></pre>
<p>可以把 presets 理解成默认基线，把额外传入的对象理解成项目级扩展层。</p>
<h3 id="tailwind-css"><a tabindex="-1" href="#tailwind-css"><span></span></a>Tailwind CSS</h3>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// oxlint.config.ts</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/oxc-integration'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineOxlintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">react</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(),</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  oxlintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">tailwindcss</span><span style="color:#383A42;--shiki-dark:#D4D4D4">({</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    entryPoint</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> './src/styles.css'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }),</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span></code></pre>
<p>Tailwind preset 会通过 Oxlint JS plugins 使用 <code>eslint-plugin-better-tailwindcss</code>，让 Tailwind CSS v3 和 v4 项目共用同一套 class linting 规则。</p>
<h2 id="eslint-fallback-快速开始"><a tabindex="-1" href="#eslint-fallback-快速开始"><span></span></a>ESLint Fallback 快速开始</h2>
<h3 id="vue--next--复杂-react-项目"><a tabindex="-1" href="#vue--next--复杂-react-项目"><span></span></a>Vue / Next / 复杂 React 项目</h3>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-js"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// eslint.config.js</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineEslintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">eslintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/oxc-integration'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineEslintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  eslintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">imports</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(),</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  eslintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">react</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(),</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  eslintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">tailwindcss</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(),</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  eslintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">vitest</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(),</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span></code></pre>
<p>如果是 Vue 项目，把 <code>react()</code> 换成 <code>vue()</code> 即可。
如果是 Next 项目，请使用 <code>next()</code>。
只有项目真的在使用 Tailwind CSS 时，才需要再加上 <code>tailwindcss()</code>。</p>
<h3 id="markdown-内容-lint"><a tabindex="-1" href="#markdown-内容-lint"><span></span></a>Markdown 内容 lint</h3>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-js"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// eslint.config.js</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineEslintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">eslintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/oxc-integration'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineEslintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">eslintPresets</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">markdown</span><span style="color:#383A42;--shiki-dark:#D4D4D4">())</span></span></code></pre>
<p>如果你只想为 Markdown 文件开启独立的 lint 行为，而不希望把它并入
<code>lint-md</code> 或基于 Prettier 的内容工作流，可以使用 <code>markdown()</code>。</p>
<h2 id="oxfmt-快速开始"><a tabindex="-1" href="#oxfmt-快速开始"><span></span></a>Oxfmt 快速开始</h2>
<p>建议优先使用带类型提示的 <code>oxfmt.config.ts</code>，这样配置结构会与官方 formatter API 保持一致。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// oxfmt.config.ts</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'oxfmt'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">oxfmtConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/oxc-integration'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#E45649;--shiki-dark:#9CDCFE">oxfmtConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span></code></pre>
<p>如果你想覆盖默认值：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// oxfmt.config.ts</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'oxfmt'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">getOxfmtConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/oxc-integration'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">  getOxfmtConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">({</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    semi</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#986801;--shiki-dark:#569CD6"> true</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }),</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span></code></pre>
<p><code>oxfmt .</code> 会自动发现 <code>oxfmt.config.ts</code>，所以通常不需要额外显式传入配置路径。</p>
<p>如果你需要官方 formatter 类型，请直接从 <code>oxfmt</code> 包导入，而不是从本包导入。</p>
<h2 id="推荐脚本"><a tabindex="-1" href="#推荐脚本"><span></span></a>推荐脚本</h2>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "scripts"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "lint"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"oxlint ."</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "lint:eslint"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"eslint ."</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "lint:full"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"npm run lint &#x26;&#x26; npm run lint:eslint"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "format"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"oxfmt ."</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>推荐用法：</p>
<ul>
<li><code>base</code> / <code>node</code> 项目先从 <code>lint</code> 开始</li>
<li><code>vue</code>、<code>next</code>、以及较复杂的 <code>react</code> 项目默认建议使用 <code>lint:full</code></li>
</ul>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[Vue Picture Cropper 发布 1.x 版本 讲一讲背后的设计理念]]></title>
            <link>https://chengpeiquan.com/article/vue-picture-cropper-v1-desgin-philosophy</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/vue-picture-cropper-v1-desgin-philosophy</guid>
            <pubDate>Tue, 24 Feb 2026 09:05:02 GMT</pubDate>
            <content:encoded><![CDATA[<p>这个春节假期对 <a href="https://github.com/chengpeiquan/vue-picture-cropper">vue-picture-cropper</a> 这个包进行了一次改版，主要想解决一些工程设计上的老问题，例如 <a href="https://github.com/chengpeiquan/vue-picture-cropper/issues/49">#49</a> 、 <a href="https://github.com/chengpeiquan/vue-picture-cropper/issues/45">#45</a> 这些 issue 提到的问题。</p>
<p>虽然是个 Breaking Change ，但对用户来说迁移成本不大，并且有了一些更灵活的用法（例如组合式函数），我自己则在重写源码时积累了一些思考点，在这篇文章里分享一下。</p>
<p>本文说明 vue-picture-cropper 从 v0.x 升级到 v1.x 时，在打包方式、样式加载和实例管理上的设计取舍与原因。</p>
<h2 id="the-history-of-this-package"><a tabindex="-1" href="#the-history-of-this-package"><span></span></a>为什么会有这个包</h2>
<p>在谈 1.x 的设计之前，有必要先回顾一下 0.x 的初衷。</p>
<p>0.x 的第一个版本发布于 2020 年 11 月。当时 Vue 3.x 刚刚发布，整体生态尚处于早期阶段，许多常用库还未完成适配，工程实践也尚未完全稳定。这导致当时的 Vue 3 业务项目很容易因为生态不足而延误工期。</p>
<p>因此，这个包最初并不是面向通用场景的组件库，而是一个为业务项目快速适配的个人小工具。那个时候我的设计目标非常明确：</p>
<ul>
<li>快速为 Vue 3 提供一层可用的 Cropper 封装</li>
<li>尽可能降低接入成本</li>
<li>以 “开箱即用” 为优先原则</li>
</ul>
<p>因此在 0.x 中选择将 cropperjs 内置打包，用户只需安装一个包即可使用。</p>
<p>这个处理方式从当时的设计目标来看，是合理的取舍 —— 它优先解决了 “可用性” 和 “便捷性” 的问题，而不是工程边界与依赖模型的完备性。</p>
<p>但这几年随着项目逐步被更多场景使用，工程规模扩大、依赖关系复杂化，这种早期的便捷型设计也逐渐暴露出局限性。</p>
<p>1.x 的调整，并不是对 0.x 的否定，而是在使用场景变化之后的一次架构升级。</p>
<h2 id="why-is-it-still-cropperjs-v1"><a tabindex="-1" href="#why-is-it-still-cropperjs-v1"><span></span></a>为什么仍然是 Cropper.js 1.x</h2>
<p>虽然 Cropper.js 主分支已经切换到 2.x，但 v1 与 v2 在架构和使用方式上存在显著差异：</p>
<table>
<thead>
<tr>
<th align="center">对比维度</th>
<th align="left">1.x 特点</th>
<th align="left">2.x 特点</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><strong>架构</strong></td>
<td align="left">传统单体 JavaScript 库，所有 API 通过构造函数和配置项提供</td>
<td align="left">基于 Web Components（自定义元素）重构，将不同能力拆分成可组合的元素（如 <code>&#x3C;cropper-image></code>、<code>&#x3C;cropper-selection></code>），表达方式更偏 “原生 DOM 组件化”</td>
</tr>
<tr>
<td align="center"><strong>API 与使用模式</strong></td>
<td align="left">以配置项/方法为主，适合 Vue 或纯 JS 组件封装</td>
<td align="left">一部分 API 通过 DOM 事件、属性和自定义元素组合替代原配置项，如 viewMode、dragMode 等迁移到不同元素的属性/事件</td>
</tr>
<tr>
<td align="center"><strong>生态兼容性和迁移成本</strong></td>
<td align="left">API 形态对 Vue 封装友好，稳定且迁移成本低</td>
<td align="left">Web Components 架构现代，但与 Vue 响应式和生命周期体系存在适配成本，需要额外桥接层</td>
</tr>
<tr>
<td align="center"><strong>成熟度与稳定性</strong></td>
<td align="left">已长期稳定维护，用户基础大，语义明确</td>
<td align="left">引入现代架构思路，但升级仍需用户适配 API 和行为，版本替换不够直接</td>
</tr>
</tbody>
</table>
<p>基于以上差异，选择 v1 的工程考量如下：</p>
<ul>
<li>稳定可预测的 API，方便组件封装与调用</li>
<li>低迁移成本与低学习成本，保持本库用户现有使用习惯</li>
<li>生态友好，遇到问题更容易查找和解决</li>
<li>面向现状工程使用，确保构建、SSR 和多实例场景可控</li>
</ul>
<p>因此，本库依然选择依赖 Cropper.js 1.x ，而非 2.x ，以保证成熟稳定、可维护和生态兼容性。</p>
<p>如果希望使用 Cropper.js 2.x 的现代 Web Components 架构，建议直接使用 Cropper.js 原生库，而非本库的 Vue 封装层。</p>
<h2 id="why-esm-only"><a tabindex="-1" href="#why-esm-only"><span></span></a>为什么仅提供 ESM 版本</h2>
<p>从 1.x 开始，本库仅以 ESM（ES Modules）形式发布，不再提供 CommonJS (CJS) 或 IIFE 构建版本。</p>
<p>背景与原因：</p>
<ul>
<li>现代 Vue 项目默认使用 ESM
<blockquote>
<p>绝大多数 Vue 3 项目都基于 Vite 或其他现代打包工具，这些环境原生支持 ESM。提供 CJS 或 IIFE 构建在这些场景下意义不大，同时增加维护成本。</p>
</blockquote>
</li>
<li>IIFE / CDN 使用量极低
<blockquote>
<p>通过 CDN 分发 IIFE 构建的场景在实践中非常少见，本库统计和社区反馈均显示几乎无人使用。继续提供会增加打包体积和测试负担，但对用户价值有限。</p>
</blockquote>
</li>
<li>简化构建与维护
<blockquote>
<p>移除 CJS/IIFE 后，库的构建流程更简单，TypeScript 类型和模块导出更一致，也避免了 CJS 下 default + named export 的潜在混乱问题。</p>
</blockquote>
</li>
</ul>
<p>如果你的项目依赖 CJS / IIFE，请迁移到支持 ESM 的环境，例如使用 Vite、Nuxt 或现代 Webpack 版本。</p>
<h2 id="bundle-adjustments"><a tabindex="-1" href="#bundle-adjustments"><span></span></a>关于 Bundle 的调整</h2>
<p>在 0.x 中，Cropper.js 作为内部依赖被打包进 <code>vue-picture-cropper</code> 的产物中，用户只需安装一个包即可使用：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 0.x 时的安装方式</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">npm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> i</span><span style="color:#50A14F;--shiki-dark:#CE9178"> vue-picture-cropper</span></span></code></pre>
<p>而在 1.x ，Cropper.js 不再被打包进本库的 Bundle ，需要由使用方在项目中<strong>显式安装并锁定</strong> Cropper.js 1.x：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 1.x 需要这么安装</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">npm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> i</span><span style="color:#50A14F;--shiki-dark:#CE9178"> vue-picture-cropper</span><span style="color:#50A14F;--shiki-dark:#CE9178"> cropperjs@^1</span></span></code></pre>
<p>虽然 0.x 看起来方便，但在工程项目里存在这样的问题：</p>
<blockquote>
<p>cropperjs 被打包进 vue-picture-cropper ，如果用户项目中也单独使用了 cropperjs ，或其他库也依赖 cropperjs ，就可能出现：多份 cropperjs 、多个实例冲突、工程产物体积变大</p>
</blockquote>
<p>简单来说，0.x 是 “内置运行时依赖的封装组件” ，而 1.x 是 “对等依赖（peer dependency）模式下的 Vue 适配层” 。</p>
<p>这样做带来的好处，体现在在运行时层面是：</p>
<ul>
<li>避免重复打包与多实例问题（由宿主项目统一管理版本，只会存在一份依赖）</li>
<li>保证运行时构造函数来源唯一</li>
<li>降低原型链与 instanceof 判断异常的风险</li>
</ul>
<p>体现在工程层面是：</p>
<ul>
<li>将版本控制权交还给项目本身</li>
<li>避免 “幽灵依赖” 式的隐式依赖关系</li>
<li>消除因传递依赖升级带来的不可预测风险</li>
<li>更符合现代包管理最佳实践（更小的 Bundle 、更好的 Tree-Shaking ）</li>
<li>让库职责更加单一清晰</li>
</ul>
<blockquote>
<p>注：“幽灵依赖（phantom dependency）”指代码运行时依赖某个包，但该包未在当前项目的 package.json 中显式声明。这种情况通常源于 Node.js 的模块解析机制能够访问 node_modules 中的任意已安装包，从而导致依赖关系在项目层面不可见。</p>
</blockquote>
<h2 id="load-styles"><a tabindex="-1" href="#load-styles"><span></span></a>关于样式加载方式的变更</h2>
<p>和 v0.x 不同，从 1.x 开始不再自动注入样式。</p>
<p>在 v0.x 中，样式会被打包为字符串，并在组件加载时动态创建 <code>&#x3C;style></code> 标签插入到页面中，例如：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// v0.x 时的源代码设计</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">loadRes</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/utils'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> cropperStyle</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'cropperjs/dist/cropper.css?inline'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> vpcStyle</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> './style.css?inline'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">loadRes</span><span style="color:#383A42;--shiki-dark:#D4D4D4">({</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  type</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'style'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  id</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'cropperjs'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  resource</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> cropperStyle</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">})</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">loadRes</span><span style="color:#383A42;--shiki-dark:#D4D4D4">({</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  type</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'style'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  id</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'vue-picture-cropper'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  resource</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> vpcStyle</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">})</span></span></code></pre>
<p>从 1.x 开始，需要主动在项目的入口文件导入组件样式：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// 此处是业务项目，需要主动导入 Cropper.js 样式和 VuePictureCropper 样式</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'cropperjs/dist/cropper.css'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'vue-picture-cropper/style.css'</span></span></code></pre>
<p>为什么移除自动注入？</p>
<p>自动注入样式在早期虽然方便，但也带来一些问题：</p>
<ul>
<li>可控性低：样式在组件加载时才插入，难以统一管理和覆盖全局主题</li>
<li>Tree-Shaking 无效：打包时无法有效剔除未使用的样式</li>
<li>冲突与重复：在大型工程或多实例场景下，可能会重复注入或覆盖其他样式</li>
<li>一致性问题：SSR、CSS Modules 或其他构建工具环境中，动态注入样式可能导致渲染不一致</li>
</ul>
<p>改为显式导入后：</p>
<ul>
<li>项目可以统一管理样式入口</li>
<li>支持 Tree-Shaking 和按需加载</li>
<li>与现代前端构建工具和工程实践高度兼容</li>
</ul>
<p>在 1.x ，样式加载更透明、可控、可维护，符合现代前端工程化最佳实践。</p>
<h2 id="obtain-instance"><a tabindex="-1" href="#obtain-instance"><span></span></a>关于实例的获取方式调整</h2>
<p>在 v0.x 版本中，<code>cropper</code> 实例是通过模块级变量管理的：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// 0.x 源码设计</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#A626A4;--shiki-dark:#569CD6"> let</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> cropper</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> CropperInstance</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> |</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> null</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// 使用 0.x 业务</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> VuePictureCropper</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">cropper</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'vue-picture-cropper'</span></span></code></pre>
<p>这种设计带来了一些限制：</p>
<ul>
<li>多实例冲突：同一页面同时使用多个裁剪框时，必须额外包一层子组件，否则实例会互相覆盖。</li>
<li>逻辑复用不直观：开发者在多实例场景下调用方法或复用逻辑时不够方便，使用体验受限。</li>
</ul>
<p>因此 1.x 完全重构了实例管理方式：</p>
<ul>
<li>组件实例独立管理 Cropper
<blockquote>
<p>每个 VuePictureCropper 组件拥有自己的状态，不再依赖模块级变量，保证实例相互独立。</p>
</blockquote>
</li>
<li>通过组件 ref 直接获取实例
<blockquote>
<p>开发者可以安全地调用 getDataURL、getBlob、getFile 等方法，无需担心覆盖或冲突。</p>
</blockquote>
</li>
<li>支持多实例和逻辑复用更简单
<blockquote>
<p>在同一页面中同时存在多个裁剪框，各实例互不干扰，操作逻辑更清晰、可复用性更高。</p>
</blockquote>
</li>
</ul>
<p>如果希望在纯逻辑中使用裁剪能力，而不依赖模板 ref，可以使用 1.x 提供的组合式函数 <a href="https://cropper.chengpeiquan.com/zh/guide/hook-api">useCropper</a>，直接获得与实例绑定的控制器，实现逻辑层面的复用和集中管理。</p>
<h2 id="summary"><a tabindex="-1" href="#summary"><span></span></a>写在最后</h2>
<p>总的来说，1.x 在 Bundle、样式和实例三方面做了统一取舍：<strong>依赖与样式由使用方显式管理，实例与组件一一对应</strong>。这样既便于构建与 SSR，又支持多实例与逻辑复用。</p>
<p>虽然是一个 Breaking Change ，但 1.x 的 VuePictureCropper 组件 Props 仍然保持和 0.x 一致，主要有以下不同点：</p>
<ol>
<li><a href="https://cropper.chengpeiquan.com/zh/guide/migration#install-deps">依赖与版本变化</a></li>
<li><a href="https://cropper.chengpeiquan.com/zh/guide/migration#load-style">组件内置样式的载入方式变化</a></li>
<li><a href="https://cropper.chengpeiquan.com/zh/guide/migration#cropper-instance">获取 Cropper 实例的方式变化</a></li>
</ol>
<p>具体迁移步骤请参考 <a href="https://cropper.chengpeiquan.com/zh/guide/migration">从 v0.x 迁移</a> 以及 <a href="https://cropper.chengpeiquan.com/zh/examples/basic-component">在线示例</a> 。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[年终总结：2025 年的一些回顾和 2026 年的一些小规划]]></title>
            <link>https://chengpeiquan.com/article/2025-year-end-summary</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/2025-year-end-summary</guid>
            <pubDate>Wed, 31 Dec 2025 15:31:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>2025 年过得真是快，感觉比以往都要快。</p>
<h2 id="开源社区与-ai-编程"><a tabindex="-1" href="#开源社区与-ai-编程"><span></span></a>开源社区与 AI 编程</h2>
<p>梳理了这一年的变化，还是先从开源说起吧，今年在 GitHub 上的活跃度，勉强保持了打卡式活跃……</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010018408.jpg?x-oss-process=image/interlace,1" alt="2025 年的活跃情况">
<p>搞错了，这是黄村地铁站的 Commit ……</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010018409.jpg?x-oss-process=image/interlace,1" alt="广州 4 号线黄村地铁站的提交墙啊哈哈哈">
<p>哈哈哈哈哈拍摄于 2025-12-30 晚上下班后，估计有人看到我在那拍，毕竟那是个人来人往的换乘站，还是在人最多的拐弯处，大家都在赶路，就我突然站在那对着墙拍照！</p>
<p>我的活跃在这里，截图还是生成自 <a href="https://github-contributions.vercel.app">GitHub Contributions</a> ，用了好几年了这个工具。</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010024549.jpg?x-oss-process=image/interlace,1" alt="这两年在 GitHub 的活跃情况">
<p>活跃度断崖式下降，有效的新开源工具 0 ，新项目 0 。</p>
<p>因为 AI 辅助编程的发力，从三月份开通 Cursor Pro 开始，加上后面交叉使用的 GPT 、 Claude 、Gemini …… ，几乎全年都在 Vibe Coding ，日常已经转为一名 Prompt 工程师……</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010047373.jpg?x-oss-process=image/interlace,1" alt="Cursor 的 Token 消耗量是 603.1M">
<p>AI 发力带来的好处就是提高了生产力，解放了劳动力；带来的副作用就是好像一瞬间失去了想做点什么的动力，好像什么都能用 AI 搞，好像什么都没必要分享了，都是问一下 AI 就行。</p>
<p>所以今年一直只是在私有仓库里尝试一些乱七八糟的东西，或者写点私有笔记，真正拿来搞点什么东西分享到开元社区的，没有，下次一定！</p>
<h2 id="这一年的工作"><a tabindex="-1" href="#这一年的工作"><span></span></a>这一年的工作</h2>
<p>在去年 <a href="/article/true-colors-a-decade">2024 的年终总结</a> 里提到公司的主力项目今年要 Release ，做到了，包括相关的硬件产品也推出了几个比较基础的型号，市场反馈和用户反馈还可以，这一年的付出还是得到了肯定。</p>
<p>年底公司也组织了一次团建，那天上午还开了个总结会，然后分组讨论讨论讨论着被我们产品妹子推上去讲了两句，留了张年度照片（就是旁边玩手机这个人……），哈哈哈哈我就不露脸了，露个文身看得出是我就行。</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010128161.jpg?x-oss-process=image/interlace,1" alt="广州真热啊，12 月我还短裤短袖">
<p>前面说到在开源一点都没产出，不过在公司里产出还算可以吧，AI 解放了双手，有更多的时间去思考和沉淀，留下了几十篇技术文档，完成的需求功能也不算少，几乎每个版本都有自己实现的东西，做的东西有人用，不论是开源还是公司产品，这一点都是成就感满满的！</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010144735.jpg?x-oss-process=image/interlace,1" alt="每一篇还挺长的，所以博客也没怎么更新，都写公司内部去了">
<p>总的来说工作方面我对自己也是挺满意的，有一些成果算是超出我自己的预期，虽然也依然有一些属于我自己的能力欠缺以及业务痛点还需要解决。</p>
<p>这一年公司也入职了很多技术超级无敌强的大佬，性格也都很好，很务实很沉稳，能一起共事真是十分荣幸，新的一年公司产品线也还会继续壮大，应该也是继续很忙，下一年的总结应该会更好。</p>
<p>另外，还有了一件很有纪念意义的工作服，设计灵感来自我们的系统界面，笑死，以后写 BUG 小心点，不然穿出去被人追着打...</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010255616.jpg?x-oss-process=image/interlace,1" alt="专属工作服">
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010255615.jpg?x-oss-process=image/interlace,1" alt="OS 在控制台的界面">
<h2 id="关于生活"><a tabindex="-1" href="#关于生活"><span></span></a>关于生活</h2>
<p>这一年依然选择了宅了一年，哪也没去玩，不过翻阅过去一年的朋友圈，也是有一些有意思的事情发生。</p>
<p>简单摘录一点，也不放太多哈哈哈哈，不然就是流水账了！</p>
<h3 id="加了个文身"><a tabindex="-1" href="#加了个文身"><span></span></a>加了个文身</h3>
<p>六月份终于把三年前就想纹的图案构思了出来，两株麦穗，源自广州的别名 “穗城” 和她的 “五羊衔谷” 传说，从我的贝斯两侧缓缓伸出，绕肩而上，同根生长。</p>
<p>这个图案名为《同根》，其实也是我最喜欢的吉他手黄贯中的一张专辑名称，<a href="https://music.apple.com/song/%E5%90%8C%E6%A0%B9/1443387512">同根</a> 也是这张专辑的同名主打歌曲，歌曲的创作与饥荒和互助有关，这个文身的出处也是。</p>
<p>另外对于我来说，还有另外一层意义，广州这座城市在我年龄还是个位数的时候，浓厚的历史沉淀就对我有着精神上的吸引，长大后在这里读书再到出社会独立，又从物质上帮我扎下了根，直到现在依然喜欢这座城市。</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010202207.jpg?x-oss-process=image/interlace,1" alt="文身那天发的朋友圈">
<img src="https://cdn.chengpeiquan.com/img/2025/07/202507160057018.jpg?x-oss-process=image/interlace,1" alt="现在我的右手是这样">
<p>全部文身可以在 <a href="/tattoos">文身专栏</a> 找到，这里记载了每个文身的意义。</p>
<h3 id="重新设计了博客"><a tabindex="-1" href="#重新设计了博客"><span></span></a>重新设计了博客</h3>
<p>四月份那会想重新设计博客首页，给博客找了好久 Hero 区域的素材搭配，最终敲定的方案又回到 “最好的素材就在身边” 的原则，用自己的第一个文身。</p>
<video src="https://cdn.chengpeiquan.com/video/2025-blog-hero-preview.mp4" poster="https://cdn.chengpeiquan.com/img/2026/01/202601010242618.jpg?x-oss-process=image/interlace,1" title="全新的 Hero 设计" controls preload="metadata" class="w-full aspect-video rounded-lg"><p><a href="https://cdn.chengpeiquan.com/video/2025-blog-hero-preview.mp4">https://cdn.chengpeiquan.com/video/2025-blog-hero-preview.mp4</a><a href="https://cdn.chengpeiquan.com/img/2026/01/202601010242618.jpg?x-oss-process=image/interlace,1">https://cdn.chengpeiquan.com/img/2026/01/202601010242618.jpg?x-oss-process=image/interlace,1</a>全新的 Hero 设计</p></video>
<p>如今也快过去一年了，还是特别喜欢，越看越喜欢，包括我的手机壁纸也一直是这个文身。</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010250031.jpg?x-oss-process=image/interlace,1" alt="手机壁纸">
<h3 id="文身专栏"><a tabindex="-1" href="#文身专栏"><span></span></a>文身专栏</h3>
<p>重新设计博客的时候，也顺带设计了我的文身专栏，第一次这么正式向大家介绍我的文身和它们背后的故事，以及合作了十年的纹身师!</p>
<p>再贴下传送门：<a href="/tattoos">文身专栏</a> 。</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010252561.jpg?x-oss-process=image/interlace,1" alt="我的文身和它们背后的故事">
<h3 id="手写的贺卡"><a tabindex="-1" href="#手写的贺卡"><span></span></a>手写的贺卡</h3>
<p>一对一轻量级资助了个山区小女生一点点费用，竟然收到教育局寄来的小朋友手写贺卡，看到这个涂改液，小时候自己涂改作业的事情回忆起来真美好！云养女儿的感觉！</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010259235.png?x-oss-process=image/interlace,1" alt="小朋友的手写贺卡">
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010259234.jpg?x-oss-process=image/interlace,1" alt="放大看！这个涂改液太有回忆了">
<h3 id="还没全秃"><a tabindex="-1" href="#还没全秃"><span></span></a>还没全秃</h3>
<p>虽然这两年明显感觉到发量在变少，但还好还没全秃，而且有时候起床后蓬松感还不错！</p>
<p>要是哪天快秃了，我就去搞个脏辫，脏辫玩够了就剃光头去！</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010307090.png?x-oss-process=image/interlace,1" alt="蓬松感带来一天的好心情">
<h3 id="发发发"><a tabindex="-1" href="#发发发"><span></span></a>发发发</h3>
<p>原来真有人随机到 888 的咖啡订单号码，是谁？是我！</p>
<img src="https://cdn.chengpeiquan.com/img/2026/01/202601010307091.png?x-oss-process=image/interlace,1" alt="发发发">
<h2 id="让摇滚的声音响彻整个夜晚"><a tabindex="-1" href="#让摇滚的声音响彻整个夜晚"><span></span></a>让摇滚的声音响彻整个夜晚</h2>
<p>去年的总结《<a href="/article/true-colors-a-decade">本色十年</a>》回忆了十年来的一些工作与生活变化，包括转岗的坚持等一些心路历程。</p>
<p>如今在 2025 年往前看十年，其实 2015 年也是挺有意义的一年，那一年鼓起勇气入职了网易，度过了对我人生十分重要的五年，后来离职的时候还写了一篇《<a href="/article/my-five-years-working-at-netease">让摇滚的声音响彻整个夜晚</a>》记录我在网易五年里的工作与生活，都是特别美好的回忆！</p>
<p>接下来是 2026 年了，也开始踏上了我文身十年，以及养猫十年的时间点，回头看过去，人生的每一步不在于是否都能踩对，但如果能留下一些有意思的事情，每次往回看十年前，依然能开心，这辈子也就值了。</p>
<h2 id="新的一年"><a tabindex="-1" href="#新的一年"><span></span></a>新的一年</h2>
<p>回到最开始，之所以先说开源和 AI ，就是想说我这一年在工作之外其实是有点缺少目标感，说白了就是迷茫。</p>
<p>尽管也学了不少新东西，在工作上也用得游刃有余，但就是总觉得没什么很垂直方向的沉淀，大部分时间都在创造当前的价值优先，缺少一点可持续发展的安全感。</p>
<p>特别是负责了一个很需要专业知识的项目也有一年多了，虽然目前也运行的还行，有吐槽，但不算多，但始终还是处于赶需求赶需求的状态，大部分问题和实现都懂，但在这个领域还是缺少更多的专业知识学习和实际探索，时间不够用。</p>
<p>上一个 OKR 周期在向老板复盘汇报的时候我也说，我始终不敢说这个项目是我在负责，因为我觉得还没有做到我自己很满意的程度，没脸公开啊哈哈，只能做一个默默维护的神秘客。</p>
<p>前两年的总结没有定下什么目标，今年希望能安排好自己的时间去钻研下，最好也仍然能沉淀一些自己的经验记录。</p>
<p>另外也减少点有的没的时间浪费，新东西层出不穷，倒也没必要到处尝试，今年就是一个浪费时间的例子，笑死。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[Obsidian x 飞牛 NAS：打造免费的跨平台笔记同步与备份方案]]></title>
            <link>https://chengpeiquan.com/article/free-note-sync-via-fnnas-and-obsidian</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/free-note-sync-via-fnnas-and-obsidian</guid>
            <pubDate>Wed, 08 Oct 2025 15:42:03 GMT</pubDate>
            <content:encoded><![CDATA[<p>从 2019.08.01 重新开始写日记，到今天居然坚持了 6 年了，一开始是记录在一款云笔记 App 上，直到四个月前陆陆续续把数据迁移到自己家里的 NAS ，把数据爬回来才发现居然接近 8 GB …… 一直觉得好像都是文字为主，没想到也配了不少图片，重新看的时候生活还挺丰富多彩的哈哈哈！</p>
<p>这里的日记就很纯粹的记录生活，不是什么读书笔记、技术笔记等等，那一类的记录对我来说都属于生活之外的事情，冷冰冰没有个人感情，生活日记记录了我每一段生活的喜怒哀乐，还有各种胡思乱想，闲着无聊的时候翻一翻，能够回顾自己过往的生活和成长，别有一番乐趣。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/10/202510082103234.jpg?x-oss-process=image/interlace,1" alt="哈哈哈很多都是跟猫生活的美好瞬间">
<h2 id="早期的方案"><a tabindex="-1" href="#早期的方案"><span></span></a>早期的方案</h2>
<p>2019 年重新开始写日记那会，我当时的需求主要是晚上睡觉前用手机记录当天的日记，并且有网页版或者桌面客户端可以平时在电脑上看一看稍微管理一下就足够，但那个时候还没什么特别流行的跨平台笔记方案，而且大部分流行的笔记 App 都是广告很多。</p>
<p>加上当时还没有开始接触 NAS ，也没有选择自建存储的想法，后面选择了一款界面比较清爽、自我感觉用户体量比较小的 App（猜的…… ），由于那家公司本职不是做这个笔记 App 产品，所以对 App 利益相关的运营干涉不是太多，所以 “清爽的体验” 一用就这么用了五年多。</p>
<p>但随着长时间深入使用，这也变成了我不想用它的原因，由于 App 不够被重视，以至于多年未维护更新，产生的 BUG 也没有修复，一度让我自己想抽空做一个客户端自己用（根据它的 BUG 表现推测是比较老版本的 React Native 做的）。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/08/202508172109712.jpg?x-oss-process=image/interlace,1" alt="这款 App 的用户体验让我越来越难以忍受">
<h2 id="新的迁移方案"><a tabindex="-1" href="#新的迁移方案"><span></span></a>新的迁移方案</h2>
<p>不过由于工作太忙， “做一个情怀客户端自己用” 这个事情也就一直放着，可以看到我是 23 年开的 Project ，直到现在都没动哈哈哈。</p>
<p>不过拖延症有拖延症的好处，因为这几年陆陆续续听到 Obsidian 、 Logseq 、 Joplin 、 Notion ，以及国产的为知笔记、思源笔记，以及语雀、飞书文档这一类不太纯笔记但也提供了很优秀的笔记功能的产品，有这么多现成的，干嘛还自己搞呢？</p>
<p>而且在开 Project 的那段时间，也开始玩起了 NAS （详见 <a href="/article/my-custom-nas">我的第一台 NAS</a> 一文），玩熟悉之后，用 NAS 来存储这些相对敏感的数据，应该说是最好的选择了。</p>
<h2 id="迁移之前的考虑"><a tabindex="-1" href="#迁移之前的考虑"><span></span></a>迁移之前的考虑</h2>
<p>有了数据迁移的想法，先梳理看看自己都需要些啥功能。</p>
<h3 id="明确自己的需求"><a tabindex="-1" href="#明确自己的需求"><span></span></a>明确自己的需求</h3>
<p>在现阶段，用 NAS 作为数据存储是已经确定的事情，剩下的不确定因素主要还是客户端方案的选择。</p>
<table>
<thead>
<tr>
<th align="center">需求点</th>
<th align="left">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">私有化部署</td>
<td align="left">考虑到自己当前的需求主要是个人日记，偏隐私，我选择将数据存放到我的 NAS 上</td>
</tr>
<tr>
<td align="center">多平台客户端</td>
<td align="left">我自己主要设备是需要有 iOS App 和 macOS App (或者 Web )</td>
</tr>
<tr>
<td align="center">客户端响应快</td>
<td align="left">不论是启动速度，还是搜索速度，因为写了 6 年，几千篇笔记，速度方面还是有要求的</td>
</tr>
<tr>
<td align="center">交互体验好</td>
<td align="left">想换掉之前的方案就是因为体验太差了，至少不能再有我上面提到的那些问题</td>
</tr>
<tr>
<td align="center">界面颜值高</td>
<td align="left">我当时选那个 App 的原因就是清爽，虽然没有暗模式，但亮模式的 UI 很像 <a href="https://ui.shadcn.com">Shadcn UI</a></td>
</tr>
<tr>
<td align="center">多端同步</td>
<td align="left">最重要的功能，可以直接与我的 NAS 进行数据传输</td>
</tr>
</tbody>
</table>
<h3 id="客户端之间的选择"><a tabindex="-1" href="#客户端之间的选择"><span></span></a>客户端之间的选择</h3>
<p>支持私有部署的 “单机笔记” 方面，主流的就是 Obsidian 、 Logseq 、 Joplin 、为知笔记、思源笔记 这几款。</p>
<p>基于社区评价、私有化部署的内存占用（ NAS 比较重视）、客户端颜值等维度对比后，就剩下 Obsidian 和 Logseq 两者， Logseq 是比 Obsidian 要晚一点推出的产品，所以它具备了 OB 的一些优点，同时还具有双向链接、块引用等更现代化的功能。</p>
<p>不过社区普遍认为 Logseq 在处理大量笔记时性能稍弱一些，而它基于大纲的组织方式，对我目前以 “日记” 为主的使用场景来说略显复杂。</p>
<p>未来如果要记录其他类型的内容，我可能会选择 Logseq 来做区分，但目前还是选了更为经典的 Obsidian 。</p>
<h2 id="目前的方案"><a tabindex="-1" href="#目前的方案"><span></span></a>目前的方案</h2>
<p>接下来讲讲我目前确定下来的具体架构和配置。</p>
<h3 id="多端同步架构"><a tabindex="-1" href="#多端同步架构"><span></span></a>多端同步架构</h3>
<p>经过前面的方案对比之后，我的笔记方案整体架构非常简单。</p>
<ul>
<li>NAS 作为数据存储中心，并通过自带的 WebDAV 服务对外同步</li>
<li>多端则使用 Obsidian 官方提供的桌面和手机客户端，配好对应的同步功能即可和 NAS 进行数据对接</li>
</ul>
<img src="https://cdn.chengpeiquan.com/img/2025/10/202510040114662.jpg?x-oss-process=image/interlace,1" alt="以 NAS 为中心的架构图">
<p>在同步方案的选择上，基于 NAS ，就毫无悬念选 WebDAV 了：</p>
<ul>
<li>✅ 数据完全私有，不依赖第三方服务</li>
<li>✅ 局域网内同步速度极快，也可以配合 DDNS 或内网穿透实现外网访问</li>
<li>✅ 理论上无限容量，无需额外的订阅成本（硬盘成本算到 NAS 的购买成本里了）</li>
<li>✅ Obsidian 有对应的插件支持 WebDAV</li>
</ul>
<h3 id="其他方案对比"><a tabindex="-1" href="#其他方案对比"><span></span></a>其他方案对比</h3>
<p>不过 Obsidian 也有其他的同步方案，这里也顺便记录下查过的方案对比，供参考：</p>
<table>
<thead>
<tr>
<th align="center">方案</th>
<th align="left">优点</th>
<th align="left">缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><strong>Obsidian Sync</strong></td>
<td align="left">官方方案，稳定可靠</td>
<td align="left">需要订阅，最低 $4 / 月，并且限制 1 GB 上限，单个文件不能超过 5 MB</td>
</tr>
<tr>
<td align="center"><strong>iCloud Drive</strong></td>
<td align="left">苹果生态无缝集成，有基础的免费容量</td>
<td align="left">真正用起来的话需要订阅，最低 ￥6 / 月可以达到 50 GB ，否则只有 5 GB 可用，另外注意这些方便服务仅限苹果设备</td>
</tr>
<tr>
<td align="center"><strong>网盘</strong></td>
<td align="left">主流云盘，稳定，有基础的免费容量</td>
<td align="left">免费版速度慢，容量小，也是需要订阅提升体验，但依然有容量限制、文件大小限制</td>
</tr>
<tr>
<td align="center"><strong>Git 同步</strong></td>
<td align="left">免费，版本控制强大，文本 Diff 对比速度快</td>
<td align="left">配置复杂，不适合非技术用户；仅对纯文本友好，不适合托管图片、视频多的笔记内容</td>
</tr>
<tr>
<td align="center"><strong>WebDAV (NAS)</strong></td>
<td align="left">服务免费，内置服务开箱即用，局域网速度超快</td>
<td align="left">需要 NAS 设备（一次性硬件投入）</td>
</tr>
</tbody>
</table>
<h3 id="飞牛同步客户端"><a tabindex="-1" href="#飞牛同步客户端"><span></span></a>飞牛同步客户端</h3>
<p>在这里还要提及一个特别的同步方案，那就是飞牛同步，支持 Windows 和 macOS 平台。</p>
<p>飞牛同步的优势在于：</p>
<ul>
<li>✅ 飞牛官方团队维护，与飞牛 NAS 深度集成</li>
<li>✅ 配置相对简单，对非技术用户更友好</li>
<li>✅ 针对飞牛 NAS 优化，同步性能可能更好</li>
</ul>
<p>可以在官网下载 <a href="https://www.fnnas.com/download?key=fn-sync-client&#x26;utm_source=chengpeiquan.com">飞牛同步客户端</a> 。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/10/202510130102237.jpg?x-oss-process=image/interlace,1" alt="飞牛同步客户端登录界面">
<p>不过目前飞牛同步还只有 PC 版本，还没有移动端版本支持。对于我这种需要在 iPhone 上记录日记的场景，WebDAV 方案配合 Obsidian App 是更合适的选择。</p>
<p>但如果你的主要使用场景是桌面端同步文件（不限于 Obsidian ），或者希望有更简单的配置流程，飞牛同步也是一个值得考虑的选择。</p>
<h2 id="使用体验"><a tabindex="-1" href="#使用体验"><span></span></a>使用体验</h2>
<p>先说说目前方案的使用体验吧，如果你觉得这套方式也适合自己，再继续往下看配置部分。</p>
<h3 id="旧数据迁移"><a tabindex="-1" href="#旧数据迁移"><span></span></a>旧数据迁移</h3>
<p>我之前用的那个笔记 App 不支持导出数据，所以我是通过 DevTools 查看它的 API 请求过程，写了个爬虫把笔记内容爬了回来。</p>
<p>对方的 API 返回的是 HTML ，因此本地又编写了一个 HTML 转 Markdown 的工具进行格式转换（推荐 <a href="https://github.com/remarkjs">Remark</a> 系列工具包）。</p>
<p>笔记中的图片原本也是远程 URL ，我在爬取时一并下载到本地，并按日期文件夹归档，再将笔记里的引用路径改成相对路径指向本地图片，当然这些工作也是用脚本处理的。</p>
<p>具体细节这里就不展开了，前端同学对这种流程应该不陌生，而且爬虫这东西也不太方便公开细说。</p>
<h3 id="使用感受"><a tabindex="-1" href="#使用感受"><span></span></a>使用感受</h3>
<p>写这篇博客时，我已经迁移到 Obsidian + 飞牛 NAS 四个月了，总体体验可以说是非常满意：</p>
<p><strong>优点：</strong></p>
<ul>
<li>✅ <strong>速度快</strong>：局域网同步几乎是秒传，搜索 2000+ 笔记也很流畅</li>
<li>✅ <strong>稳定性好</strong>：至今未出现同步失败或数据丢失</li>
<li>✅ <strong>界面清爽</strong>：Obsidian 的界面简洁干净，用起来非常舒适</li>
<li>✅ <strong>插件丰富</strong>：几乎所有功能都有对应插件支持自定义</li>
<li>✅ <strong>数据安全</strong>：可以只在局域网内同步，数据不出网更安全</li>
<li>✅ <strong>备份方便</strong>：在 NAS 端还可以通过 “文件备份” 功能自动备份到其他存储位置</li>
</ul>
<p><strong>需要注意的点：</strong></p>
<ul>
<li>⚠️ <strong>初次配置有门槛</strong>：NAS 端配置简单，但 Obsidian 的客户端选项较多，需要点时间摸索</li>
<li>⚠️ <strong>外网访问速度</strong>：若 NAS 没有公网 IP ，使用 FN Connect 免费版的访问速度可能偏慢（可考虑付费版）</li>
<li>⚠️ <strong>移动端容量</strong>：Obsidian 是本地化编辑器，所有文件都会存储在设备上，需确保手机空间充足</li>
<li>⚠️ <strong>预防同步冲突</strong>：多设备同时编辑时，仍需留意冲突问题</li>
</ul>
<h2 id="多端同步配置"><a tabindex="-1" href="#多端同步配置"><span></span></a>多端同步配置</h2>
<p>接下来讲讲怎么围绕 NAS 这个数据中心实现多端同步，主要以飞牛 NAS 端，以及 Obsidian 桌面客户端，先把流程跑通了，在 Obsidian App 的设置也是一样的。</p>
<h3 id="飞牛-nas-配置"><a tabindex="-1" href="#飞牛-nas-配置"><span></span></a>飞牛 NAS 配置</h3>
<p>下面的配置步骤都是以 Web 端的操作为例，在飞牛的 App 操作也是类似，按顺序操作即可，需要注意的是，请使用管理员账号 <strong>登录飞牛 NAS</strong> ，而不要使用普通账号，很多操作需要管理员才可以设置。</p>
<h4 id="创建笔记存储目录"><a tabindex="-1" href="#创建笔记存储目录"><span></span></a>创建笔记存储目录</h4>
<p>建议在 “文件管理” 中创建一个专门的文件夹，作为数据的存储根目录，比如 <code>database</code> ，这样其他类似的数据托管都可以存档在该文件夹里。</p>
<p>真正存放数据的地方，可以根据需要再建一层目录，例如我的日记是放在 <code>database</code> 的 <code>diary</code> 下。</p>
<blockquote>
<p>重要数据建议存放在一个有数据保护的存储空间下，预算不高的话可以像我一样，用两块 2TB 的硬盘创建一个 RAID 1 。</p>
</blockquote>
<img src="https://cdn.chengpeiquan.com/img/2025/10/202510062119261.jpg?x-oss-process=image/interlace,1" alt="创建数据根目录">
<h4 id="配置-webdav-服务"><a tabindex="-1" href="#配置-webdav-服务"><span></span></a>配置 WebDAV 服务</h4>
<p>飞牛 NAS 自带 WebDAV 服务，访问 <strong>“系统设置 → 文件共享协议 → WebDAV ”</strong> ，启用服务。默认的 HTTP 端口号 5005 / HTTPS 端口号 5006 可以直接使用，也可以自行修改。</p>
<p>再点击 WebDAV 界面上的 “设置可见文件夹范围” ，设置允许通过 WebDAV 访问的目录范围，根据自己的需要去限制范围，记得包含刚才的数据文件夹就行。</p>
<blockquote>
<p>在 NAS 的 WebDAV 配置界面上，可以看到自己的访问地址是</p>
<ul>
<li><code>http://{你的 NAS 内网 IP}:{端口号}/</code></li>
<li><code>http://{你的 NAS 域名}:{端口号}/</code></li>
</ul>
<p>注意，使用 HTTP 或 HTTPS 时，两者的端口号是不一样的</p>
</blockquote>
<p>如果需要使用域名访问 WebDAV ，可以在 <strong>“系统设置 → 远程访问”</strong> 管理 FN Connect 账号，或者在 DDNS 配置域名，具体在这里不过多介绍，飞牛的界面操作还是很清晰的。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/10/202510060101116.jpg?x-oss-process=image/interlace,1" alt="系统设置 - 文件共享协议 - WebDAV">
<h3 id="obsidian-桌面客户端配置"><a tabindex="-1" href="#obsidian-桌面客户端配置"><span></span></a>Obsidian 桌面客户端配置</h3>
<p>在 Obsidian 中，根据最流行的免费同步方案，使用了 <a href="https://github.com/remotely-save/remotely-save">Remotely Save</a> ，另外为了统一处理 Markdown 的内嵌文件路径（图片、视频等）的存放位置，我同时使用了 <a href="https://github.com/RainCat1998/obsidian-custom-attachment-location">Custom Attachment Location</a> 插件。</p>
<h4 id="仓库与日记设置"><a tabindex="-1" href="#仓库与日记设置"><span></span></a>仓库与日记设置</h4>
<p>启动 Obsidian 后会引导创建一个笔记仓库，其实就是在电脑里选择一个文件夹存档这些笔记，所选的文件夹对于这个仓库来说也是一个根目录的概念。</p>
<p>如果和我一样是用来写日记的，或者是想类似日记一样在同一个文件夹里存档笔记，并且有自己的固定笔记模板，那么可以在 “日记” 设置一些存档规则，例如我选择了将所有日记都归类到 <code>docs</code> 文件夹，而日记模板则归类在 <code>template</code> 文件夹下（模版需要具体到某一个文件的路径）。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/10/202510072232099.jpg?x-oss-process=image/interlace,1" alt="在 Obsidian 的日记设置界面，以及 Mac 对应的目录和文件">
<h4 id="内部链接设置"><a tabindex="-1" href="#内部链接设置"><span></span></a>内部链接设置</h4>
<p>Obsidian 对 Markdown 的链接和图片引用默认是它自己的语法，为了以后兼容其他客户端，建议这里也改成 “基于当前笔记的相对路径” 。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/10/202510072303736.jpg?x-oss-process=image/interlace,1" alt="将内部链接规则修改为 ”基于当前笔记的相对路径“">
<h4 id="安装插件"><a tabindex="-1" href="#安装插件"><span></span></a>安装插件</h4>
<p>插件好像都是从 GitHub 安装的，所以需要确保所处的网络环境可以顺利打开 GitHub 。</p>
<ol>
<li>打开 Obsidian 设置 → 第三方插件 → 关闭 “安全模式”</li>
<li>点击「浏览」搜索 <code>Remotely Save</code> （必要）和 <code>Custom Attachment Location</code>（可选）</li>
<li>安装对应的插件并启用</li>
</ol>
<h4 id="同步插件配置"><a tabindex="-1" href="#同步插件配置"><span></span></a>同步插件配置</h4>
<p>同步插件是核心插件，使用的是 <a href="https://github.com/remotely-save/remotely-save">Remotely Save</a> ，这里有几项需要设置：</p>
<table>
<thead>
<tr>
<th align="center">设置项</th>
<th align="left">如何设置</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">远程服务</td>
<td align="left">选择 WebDAV</td>
</tr>
<tr>
<td align="center">服务器地址</td>
<td align="left">从飞牛 NAS 复制 WebDAV 的访问地址，拼接数据库文件夹路径，例如 <code>http://192.168.8.8:5005/database</code></td>
</tr>
<tr>
<td align="center">用户名</td>
<td align="left">飞牛 NAS 里，这个 <code>database</code> 文件夹的归属账号的用户名</td>
</tr>
<tr>
<td align="center">密码</td>
<td align="left">飞牛 NAS 里，这个 <code>database</code> 文件夹的归属账号的密码</td>
</tr>
<tr>
<td align="center">并行度</td>
<td align="left">由于我只使用局域网同步，所以我开到了最大，目前可以设置 <code>20</code></td>
</tr>
</tbody>
</table>
<p>其他的就根据实际需要调整，或者保持默认就可以了。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/10/202510070232761.jpg?x-oss-process=image/interlace,1" alt="Remotely Save">
<h4 id="附件插件配置"><a tabindex="-1" href="#附件插件配置"><span></span></a>附件插件配置</h4>
<p>辅助插件目前只用了一个附件管理的 <a href="https://github.com/RainCat1998/obsidian-custom-attachment-location">Custom Attachment Location</a> ，从前面的仓库与日记设置可以看到我还有一个 <code>assets</code> 文件夹，这是因为我每天的日记除了文字，还带有不少图片，有时候还会贴视频，如果没有合理归档这些附件，混在一起就太难维护了。</p>
<table>
<thead>
<tr>
<th align="center">设置项</th>
<th align="left">如何设置</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">Location for new attachments</td>
<td align="left">我配置了 <code>Location for new attachments</code> 为 <code>../assets/${noteFileName}</code> ，这样每一篇日记的附件都会归档到 <code>assets</code> 文件夹下的 “笔记文件名” 文件夹里</td>
</tr>
<tr>
<td align="center">Should rename attachment folder</td>
<td align="left">如果笔记对应的 Markdown 文件修改了命名，它会监听并重命名这个附件文件夹，虽然我几乎不改，但一旦修改，这个功能确实挺省事的</td>
</tr>
</tbody>
</table>
<p>其他选项可以根据自己需要修改，例如图片自动重命名的一些功能。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/10/202510072239706.jpg?x-oss-process=image/interlace,1" alt="主要设置 Location for new attachments 选项">
<h3 id="obsidian-移动设备配置"><a tabindex="-1" href="#obsidian-移动设备配置"><span></span></a>Obsidian 移动设备配置</h3>
<p>我的常用移动设备是 iPhone ，所以直接在 App Store 搜索 Obsidian 即可找到客户端下载，也可以在官网找到其他端的下载。</p>
<p>安装好 App 后，在 iPhone 或其他设备上重复上述步骤进行配置。</p>
<h2 id="总结"><a tabindex="-1" href="#总结"><span></span></a>总结</h2>
<p>日常写笔记时，我的习惯是只在一端更新，写完后同步回 NAS ，下次在另一台设备启动 Obsidian 时，插件会自动比对版本，从 NAS 拉取最新数据，实现双向同步，这种方式在实际使用中没有出现文件冲突，整体体验非常稳定。</p>
<p>从 2019 年开始在第三方 App 上写日记，到现在用 Obsidian + 飞牛 NAS 搭建私有笔记系统，这 6 年的数据迁移总算告一段落。</p>
<p>这套方案在技术上并不复杂，但在内容管理理念上是一次很大的升级：</p>
<ul>
<li>数据完全自控，还可以在 NAS 上定期备份或迁移</li>
<li>所有数据本地均有存档，离线也能完整访问与编辑</li>
<li>Markdown 格式通用、可扩展，不受笔记客户端约束</li>
</ul>
<p>经过几个月使用，无论是多端同步、搜索速度还是文件安全，都比以往的云笔记方案更可靠，如果你也希望让笔记系统更可控、更长期可维护，这套组合值得一试。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[适用于 ESLint V9 的现代化扁平化配置]]></title>
            <link>https://chengpeiquan.com/article/a-modern-flat-eslint-configuration-for-eslint</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/a-modern-flat-eslint-configuration-for-eslint</guid>
            <pubDate>Thu, 13 Mar 2025 16:35:02 GMT</pubDate>
            <content:encoded><![CDATA[<p>ESLint v9.0.0 是 ESLint 的一个主要版本，它有几个重大变化，其中最大的变化是其配置文件和插件生态系统的使用，可以通过官方网站的 <a href="https://eslint.org/docs/latest/use/migrate-to-9.0.0#flat-config">迁移到 v9.x</a> 文档了解如何迁移。</p>
<p>这里有一个关于 ESLint V9 的扁平化配置的 npm 包，内置了一些个人常用的 ESLint 配置，这也是我在 GitHub 上发布的一个开源项目。如果它对您有帮助，请 <a href="https://github.com/chengpeiquan/bassist">给它一个 Star</a> ！</p>
<p>一款现代化的扁平 ESLint 配置，适用于 <a href="https://eslint.org/">ESLint</a> v9 和 v10 ，由 <a href="https://github.com/chengpeiquan">@chengpeiquan</a> 精心打造。</p>
<h2 id="-使用方法"><a tabindex="-1" href="#-使用方法"><span></span></a>⚡ 使用方法</h2>
<p>使用此 ESLint 配置仅需三步：</p>
<ol>
<li>安装依赖（参考：<a href="#-%E5%AE%89%E8%A3%85">🚀 安装</a>）</li>
<li>添加 ESLint 配置文件（参考：<a href="#-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">📂 配置文件</a>）</li>
<li>在 VS Code 的 <code>settings.json</code> 启用自动 Lint（参考：<a href="#-vs-code-%E9%85%8D%E7%BD%AE">🛠 VS Code 配置</a>）</li>
</ol>
<p>这个快速指南可以作为入门辅助，避免遗漏关键步骤 🚀 。</p>
<h2 id="-安装"><a tabindex="-1" href="#-安装"><span></span></a>🚀 安装</h2>
<p>使用常用的包管理器安装该包：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">npm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> install</span><span style="color:#986801;--shiki-dark:#569CD6"> -D</span><span style="color:#50A14F;--shiki-dark:#CE9178"> eslint</span><span style="color:#50A14F;--shiki-dark:#CE9178"> @bassist/eslint-config</span></span></code></pre>
<p><strong>注意：</strong> 支持 ESLint <code>9.x</code> 和 <code>10.x</code>，并且需要 TypeScript 版本 >= <code>5.0.0</code>。</p>
<p>如果使用的是 <code>pnpm</code>，建议在项目根目录添加 <code>.npmrc</code> 文件，并包含以下配置，以更顺利地处理 peer 依赖：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ini"><span class="line"><span style="color:#E45649;--shiki-dark:#569CD6">shamefully-hoist</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=true</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#569CD6">auto-install-peers</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=true</span></span></code></pre>
<blockquote>
<p>如果仍在使用 ESLint v8，请参考旧版（已不再维护）包：<a href="https://www.npmjs.com/package/@bassist/eslint">@bassist/eslint</a>。</p>
</blockquote>
<h2 id="-配置文件"><a tabindex="-1" href="#-配置文件"><span></span></a>📂 配置文件</h2>
<p>在项目根目录创建 <code>eslint.config.js</code> 文件：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-js"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// eslint.config.js</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">imports</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">typescript</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/eslint-config'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// 导出一个包含多个配置对象的数组</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">...</span><span style="color:#E45649;--shiki-dark:#9CDCFE">imports</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#0184BC;--shiki-dark:#D4D4D4">...</span><span style="color:#E45649;--shiki-dark:#9CDCFE">typescript</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span></code></pre>
<p>然后在 <code>package.json</code> 中添加 "type": "module" ：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "type"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"module"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "scripts"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "lint"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"eslint src"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "lint:inspector"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"npx @eslint/config-inspector"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>运行 <code>npm run lint</code> 以检查代码，或运行 <code>npm run lint:inspector</code> 在 <code>http://localhost:7777</code> 查看可视化的 ESLint 配置。</p>
<blockquote>
<p>Bun 可以直接加载 <code>eslint.config.ts</code> 。如果你通过 Node.js 运行 ESLint，建议改用 <code>eslint.config.mjs</code>，或者按照 ESLint 文档完成<a href="https://eslint.org/docs/latest/use/configure/configuration-files#typescript-configuration-files">额外的设置</a>。</p>
</blockquote>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># pnpm 工作流</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">pnpm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> lint</span></span></code></pre>
<h2 id="-jsonc-预设"><a tabindex="-1" href="#-jsonc-预设"><span></span></a>🧩 JSONC 预设</h2>
<p>如果希望 ESLint 直接检查 JSON 系列文件，可以使用 <code>jsonc</code> 预设：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-js"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// eslint.config.js</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">jsonc</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/eslint-config'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">...</span><span style="color:#E45649;--shiki-dark:#9CDCFE">jsonc</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span></code></pre>
<p><code>jsonc</code> 的默认行为：</p>
<ul>
<li>支持 <code>.json</code>、<code>.jsonc</code>、<code>.json5</code></li>
<li>默认递归排序对象 key（<code>jsonc/sort-keys</code>）</li>
<li>默认忽略 <code>package.json</code>，让包清单 key 排序继续交给格式化流程处理</li>
</ul>
<p>如果你在 VS Code 里通过 ESLint 扩展做校验，并且手动设置了
<code>eslint.validate</code>，记得把 <code>json</code> 和 <code>jsonc</code> 也加进去。</p>
<p>如果需要，也可以在项目里覆盖默认行为：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-js"><span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">jsonc</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/eslint-config'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span></span>
<span class="line"><span style="color:#0184BC;--shiki-dark:#D4D4D4">  ...</span><span style="color:#E45649;--shiki-dark:#9CDCFE">jsonc</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    files</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#50A14F;--shiki-dark:#CE9178">'**/*.json'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">'**/*.jsonc'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">'**/*.json5'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    rules</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#CE9178">      'jsonc/sort-keys'</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'off'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">    },</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  },</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span></code></pre>
<h2 id="-类型安全的配置"><a tabindex="-1" href="#-类型安全的配置"><span></span></a>✅ 类型安全的配置</h2>
<p>为了增强类型安全性，可以使用 <code>defineFlatConfig</code>:</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-js"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// @ts-check</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">defineFlatConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">imports</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">vue</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/eslint-config'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineFlatConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">([</span></span>
<span class="line"><span style="color:#0184BC;--shiki-dark:#D4D4D4">  ...</span><span style="color:#E45649;--shiki-dark:#9CDCFE">imports</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#0184BC;--shiki-dark:#D4D4D4">  ...</span><span style="color:#E45649;--shiki-dark:#9CDCFE">vue</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  // 添加更多自定义配置</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  {</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">    // 为每个配置提供名称，以便在运行 `npm run lint:inspector` 时，</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">    // 可以在可视化工具中清晰展示</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    name</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'my-custom-rule/vue'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    rules</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">      // 例如：默认情况下，该规则是 `off`</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#CE9178">      'vue/component-tags-order'</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'error'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">    },</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    ignores</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#50A14F;--shiki-dark:#CE9178">'examples'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  },</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">])</span></span></code></pre>
<h2 id="-vs-code-配置"><a tabindex="-1" href="#-vs-code-配置"><span></span></a>🛠 VS Code 配置</h2>
<p>在 VS Code 工作区的 <code>settings.json</code> 添加以下配置，以启用自动 Lint 修复：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "editor.formatOnSave"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#0184BC;--shiki-dark:#569CD6">true</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "editor.codeActionsOnSave"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "source.fixAll.eslint"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"always"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "source.fixAll.prettier"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"always"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  },</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "editor.defaultFormatter"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"esbenp.prettier-vscode"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "eslint.useFlatConfig"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#0184BC;--shiki-dark:#569CD6">true</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "eslint.format.enable"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#0184BC;--shiki-dark:#569CD6">true</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "eslint.validate"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: [</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#CE9178">    "javascript"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#CE9178">    "javascriptreact"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#CE9178">    "typescript"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#CE9178">    "typescriptreact"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#CE9178">    "json"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#CE9178">    "jsonc"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  ],</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "prettier.configPath"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"./.prettierrc.js"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>关于 <code>prettier.configPath</code> 请查看 <a href="#%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%B7%A5%E5%85%B7">格式化工具</a> 部分。</p>
<h2 id="-api-参考"><a tabindex="-1" href="#-api-参考"><span></span></a>📘 API 参考</h2>
<h3 id="defineflatconfig"><a tabindex="-1" href="#defineflatconfig"><span></span></a>defineFlatConfig</h3>
<p>定义 ESLint 配置，可选支持 Prettier 和 Tailwind CSS。</p>
<p>API 类型声明：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">/**</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * 定义 ESLint 配置，可选支持 Prettier 集成。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">param</span><span style="color:#E45649;--shiki-light-font-style:italic;--shiki-dark:#9CDCFE;--shiki-dark-font-style:inherit"> configs</span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> 基础 ESLint 配置数组。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">param</span><span style="color:#E45649;--shiki-light-font-style:italic;--shiki-dark:#9CDCFE;--shiki-dark-font-style:inherit"> options</span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> - 配置选项。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">returns</span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> 最终的 ESLint 配置数组。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> */</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">declare</span><span style="color:#A626A4;--shiki-dark:#569CD6"> const</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineFlatConfig</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  configs</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> FlatESLintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">[],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  options</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">?:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> DefineFlatConfigOptions</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#C18401;--shiki-dark:#4EC9B0"> FlatESLintConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">[]</span></span></code></pre>
<p>选项类型声明：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">interface</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> DefineFlatConfigOptions</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  /**</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * 指定用于加载 `.prettierrc` 配置的工作目录。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * 配置文件应为 JSON 格式。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">default</span><span style="color:#E45649;--shiki-light-font-style:italic;--shiki-dark:#9CDCFE;--shiki-dark-font-style:inherit"> process.cwd()</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   */</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  cwd</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">?:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> string</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  /**</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * 如果 `prettierEnabled` 设为 `false`，则所有与 Prettier 相关的规则和配置都将被忽略， 即使提供了</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * `prettierRules` 也不会生效。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">default</span><span style="color:#E45649;--shiki-light-font-style:italic;--shiki-dark:#9CDCFE;--shiki-dark-font-style:inherit"> true</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   */</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  prettierEnabled</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">?:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> boolean</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  /**</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * 默认情况下，会从当前工作目录读取 `.prettierrc`，并且 `.prettierrc` 文件必须是 JSON 格式。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * 如果配置文件不是 JSON 格式，或者使用了不同的文件名，可以将其转换为 JSON 规则后传入。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * 读取自定义配置后，会与默认的 ESLint 规则合并。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">see</span><span style="color:#E45649;--shiki-light-font-style:italic;--shiki-dark:#9CDCFE;--shiki-dark-font-style:inherit"> https://prettier.io/docs/configuration.html</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   */</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  prettierRules</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">?:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> PartialPrettierExtendedOptions</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  /**</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * Tailwind CSS 规则默认启用。如果它们影响了项目，可以通过该选项禁用。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">default</span><span style="color:#E45649;--shiki-light-font-style:italic;--shiki-dark:#9CDCFE;--shiki-dark-font-style:inherit"> true</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   */</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  tailwindcssEnabled</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">?:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> boolean</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  /**</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * 如果需要覆盖 Tailwind CSS 配置，可以传入相应的选项。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * 如果想要合并配置，可以导入 `defaultTailwindcssSettings`，手动合并后再传入。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * 如果传入空对象 `{}`，则会使用默认设置。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">see</span><span style="color:#E45649;--shiki-light-font-style:italic;--shiki-dark:#9CDCFE;--shiki-dark-font-style:inherit"> https://github.com/schoero/eslint-plugin-better-tailwindcss</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">   */</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  tailwindcssSettings</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">?:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> TailwindcssSettings</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<h3 id="creategetconfignamefactory"><a tabindex="-1" href="#creategetconfignamefactory"><span></span></a>createGetConfigNameFactory</h3>
<p><code>createGetConfigNameFactory</code> 是一个灵活的工具函数，用于生成 ESLint 配置命名工具。它可以快速拼接配置名称，确保命名空间一致，并便于组织和管理复杂的规则集。</p>
<p>API 类型声明：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">/**</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * 一个灵活的工具函数，用于生成 ESLint 配置命名工具。 它可以快速拼接配置名称，确保命名空间一致，并便于组织和管理复杂的规则集。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">param</span><span style="color:#E45649;--shiki-light-font-style:italic;--shiki-dark:#9CDCFE;--shiki-dark-font-style:inherit"> prefix</span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> - 表示配置名称前缀的字符串。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">returns</span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> 一个函数，该函数会将提供的名称片段与指定的前缀拼接在一起。</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> */</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">declare</span><span style="color:#A626A4;--shiki-dark:#569CD6"> const</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> createGetConfigNameFactory</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  prefix</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> string</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">...</span><span style="color:#383A42;--shiki-dark:#9CDCFE">names</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> string</span><span style="color:#383A42;--shiki-dark:#D4D4D4">[]) </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> string</span></span></code></pre>
<p>使用示例：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  createGetConfigNameFactory</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  defineFlatConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">} </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/eslint-config'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> getConfigName</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> createGetConfigNameFactory</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#50A14F;--shiki-dark:#CE9178">'my-prefix'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> defineFlatConfig</span><span style="color:#383A42;--shiki-dark:#D4D4D4">([</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    name</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> getConfigName</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#50A14F;--shiki-dark:#CE9178">'ignore'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">), </span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// --> `my-prefix/ignore`</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    ignores</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#50A14F;--shiki-dark:#CE9178">'**/dist/**'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">'**/.build/**'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">'**/CHANGELOG.md'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  },</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">])</span></span></code></pre>
<p>为什么要使用它？</p>
<ul>
<li>一致性：强制执行清晰统一的配置命名模式。</li>
<li>灵活性：允许为不同项目或范围自定义前缀。</li>
<li>简化管理：便于组织和浏览大型 ESLint 配置。</li>
</ul>
<p>这个工具在构建可复用的 ESLint 配置或维护复杂项目的规则集时尤其有用。</p>
<h2 id="-导出的配置"><a tabindex="-1" href="#-导出的配置"><span></span></a>📦 导出的配置</h2>
<p>这些是一些常用的配置，如果有额外需求，欢迎提交 PR！</p>
<h3 id="语言支持"><a tabindex="-1" href="#语言支持"><span></span></a>语言支持</h3>
<ul>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/javascript.ts">JavaScript</a></li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/typescript.ts">TypeScript</a></li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/jsx.ts">JSX</a></li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/jsonc.ts">JSON / JSONC / JSON5</a></li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/markdown.ts">Markdown</a></li>
</ul>
<h4 id="框架支持"><a tabindex="-1" href="#框架支持"><span></span></a>框架支持</h4>
<ul>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/next.ts">Next.js</a></li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/react.ts">React</a></li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/vue.ts">Vue (v2 and v3)</a></li>
</ul>
<h4 id="格式化工具"><a tabindex="-1" href="#格式化工具"><span></span></a>格式化工具</h4>
<p>格式化规则默认启用，不会单独导出。如需自定义配置，请通过 <a href="#defineflatconfig">defineFlatConfig API</a> 的 <code>options</code> 传入。</p>
<ul>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/private-configs/prettier.ts">Prettier</a> :
<ul>
<li>默认会读取 <code>.prettierrc</code> 和 <code>.prettierignore</code> 的内容，并添加到 ESLint 规则中。</li>
<li>如果预期的配置文件不存在，则会使用 <a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/shared/prettier-config.mjs">内置的 Prettier</a> 规则作为兜底规则。</li>
<li>非以上配置文件并且不喜欢默认规则时，可以通过 <a href="#defineflatconfig">defineFlatConfig</a> 的 <code>options.prettierRules</code> 将完整配置传递进来优先作为 ESLint 规则使用</li>
</ul>
</li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/private-configs/tailwindcss.ts">Tailwind CSS</a> :
<ul>
<li>使用 <code>eslint-plugin-better-tailwindcss</code>，让 Tailwind CSS v3 和 v4 项目共用同一套 class linting。</li>
<li>Monorepo、自定义 CSS 入口或自定义 class selector，可通过 <code>options.tailwindcssSettings</code> 传递。</li>
</ul>
</li>
</ul>
<h4 id="其它"><a tabindex="-1" href="#其它"><span></span></a>其它</h4>
<ul>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/node.ts">Node.js</a></li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/imports.ts">Imports</a></li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/regexp.ts">Regexp</a></li>
<li><a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/unicorn.ts">Unicorn</a></li>
</ul>
<h2 id="-迁移指南"><a tabindex="-1" href="#-迁移指南"><span></span></a>📚 迁移指南</h2>
<ul>
<li>扁平化配置（Flat Configs）不支持 ESLint 8.x 以下的版本。</li>
<li><code>--ext</code> CLI 选项已被移除 (<a href="https://github.com/eslint/eslint/issues/16991">#16991</a>) 。</li>
</ul>
<h2 id="-发布日志"><a tabindex="-1" href="#-发布日志"><span></span></a>📝 发布日志</h2>
<p>详细更新内容请参考 <a href="https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/CHANGELOG.md">CHANGELOG</a> 。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[解决 better-sqlite3 连接 SQLite 时报错 Could not locate the bindings file]]></title>
            <link>https://chengpeiquan.com/article/better-sqlite3-error-could-not-locate-the-bindings-file</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/better-sqlite3-error-could-not-locate-the-bindings-file</guid>
            <pubDate>Sat, 15 Feb 2025 16:42:33 GMT</pubDate>
            <content:encoded><![CDATA[<p>五年前买的阿里云 ECS 这个月底到期，前天准备续费的时候发现买个新的更划算，不仅价格差不多，还多了 1GB 内存，那还续个屌…… 服务器上要迁移的东西不多，影响不大，所以就直接买个新的了。</p>
<h2 id="简单的迁移工作"><a tabindex="-1" href="#简单的迁移工作"><span></span></a>简单的迁移工作</h2>
<p>由于老的服务器上部署的大多是前端项目（数据是连 Serverless 的 API 操作的，不在这台机器上），并且基本都是用 Docker 部署的，所以迁移工作都比较简单，在源码仓库上修改 Workflow 的目标机器 IP 和 SSH Key ，重新运行一次 CI 打包，就可以把新的镜像推送到新的服务器上了。</p>
<p>其他的像 SSL 证书， Nginx 配置，都是拷贝过去后重启一下 Nginx 就搞定，等服务都起来了，去 DNS 解析那里把域名指向新机器的 IP 就迁移完了，都问题不大。</p>
<p>除了有一个 Nest 服务，因为连了 SQLite ，迁移后出现了一点问题。</p>
<h2 id="部署后运行报错"><a tabindex="-1" href="#部署后运行报错"><span></span></a>部署后运行报错</h2>
<p>问题倒不是出在 SQLite 上，用 Docker 连接这种嵌入式数据库，都是通过 Volumes 挂载到宿主机器上的，所以访问的数据库文件路径是宿主机器上的路径，知道了这一点，把 SQLite 数据迁移到新服务器上也很简单，并且在新机器上直接用 SQLite 查询数据，也都没问题。</p>
<p>至于在 Docker 里使用 SQLite 本身，只需要在 Dockerfile 里，在安装 libc6-compat 的时候记得一起安装 sqlite 就可以。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-dockerfile"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># Dockerfile</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># Use the official Node.js image as the base image</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#569CD6">FROM</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> node:18-alpine </span><span style="color:#4078F2;--shiki-dark:#569CD6">AS</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> base</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># Install dependencies only when needed</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#569CD6">FROM</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> base </span><span style="color:#4078F2;--shiki-dark:#569CD6">AS</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> deps</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#569CD6">RUN</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> apk add --no-cache libc6-compat sqlite</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># Set the working directory inside the container</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#569CD6">WORKDIR</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> /app</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># ...</span></span></code></pre>
<p>但是 Docker 容器运行后，访问接口却挂了，通过 <code>docker logs</code> 查询容器的日志，发现启动后这里有个报错：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">[Nest] 1  - 02/15/2025, 1:23:01 PM     LOG [InstanceLoader] ScheduleModule dependencies initialized +1ms</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">[Nest] 1  - 02/15/2025, 1:23:01 PM   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">1</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)...</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Error:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Could</span><span style="color:#50A14F;--shiki-dark:#CE9178"> not</span><span style="color:#50A14F;--shiki-dark:#CE9178"> locate</span><span style="color:#50A14F;--shiki-dark:#CE9178"> the</span><span style="color:#50A14F;--shiki-dark:#CE9178"> bindings</span><span style="color:#50A14F;--shiki-dark:#CE9178"> file.</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Tried:</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/build/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/build/Debug/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/build/Release/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/out/Debug/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/Debug/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/out/Release/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/Release/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/build/default/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/compiled/18.20.6/linux/x64/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/addon-build/release/install-root/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/addon-build/debug/install-root/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/addon-build/default/install-root/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA"> →</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/lib/binding/node-v108-linux-x64/better_sqlite3.node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">    at</span><span style="color:#50A14F;--shiki-dark:#CE9178"> bindings</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (/app/node_modules/.pnpm/bindings@1.5.0/node_modules/bindings/bindings.js:126:9)</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">    at</span><span style="color:#50A14F;--shiki-dark:#CE9178"> new</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Database</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (/app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/lib/database.js:48:64)</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">    at</span><span style="color:#50A14F;--shiki-dark:#CE9178"> BetterSqlite3Driver.Database</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [as </span><span style="color:#50A14F;--shiki-dark:#CE9178">sqlite]</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (/app/node_modules/.pnpm/better-sqlite3@11.7.0/node_modules/better-sqlite3/lib/database.js:11:10)</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">    at</span><span style="color:#50A14F;--shiki-dark:#CE9178"> BetterSqlite3Driver.createDatabaseConnection</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (/app/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@11.7.0_sqlite3@5.1.7_ts-node@10.9.2_@types+node@20.17.9_typescript@5.7.2_/node_modules/typeorm/driver/better-sqlite3/BetterSqlite3Driver.js:88:41)</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">    at</span><span style="color:#50A14F;--shiki-dark:#CE9178"> async</span><span style="color:#50A14F;--shiki-dark:#CE9178"> BetterSqlite3Driver.connect</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (/app/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@11.7.0_sqlite3@5.1.7_ts-node@10.9.2_@types+node@20.17.9_typescript@5.7.2_/node_modules/typeorm/driver/sqlite-abstract/AbstractSqliteDriver.js:171:35)</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">    at</span><span style="color:#50A14F;--shiki-dark:#CE9178"> async</span><span style="color:#50A14F;--shiki-dark:#CE9178"> DataSource.initialize</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (/app/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@11.7.0_sqlite3@5.1.7_ts-node@10.9.2_@types+node@20.17.9_typescript@5.7.2_/node_modules/typeorm/data-source/DataSource.js:136:9)</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">[Nest] 1  - 02/15/2025, 1:23:04 PM   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">2</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)...</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Error:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Could</span><span style="color:#50A14F;--shiki-dark:#CE9178"> not</span><span style="color:#50A14F;--shiki-dark:#CE9178"> locate</span><span style="color:#50A14F;--shiki-dark:#CE9178"> the</span><span style="color:#50A14F;--shiki-dark:#CE9178"> bindings</span><span style="color:#50A14F;--shiki-dark:#CE9178"> file.</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Tried:</span></span></code></pre>
<h2 id="错误日志分析"><a tabindex="-1" href="#错误日志分析"><span></span></a>错误日志分析</h2>
<p>在 Node 服务端程序连接 SQLite 是用了 <a href="https://github.com/WiseLibs/better-sqlite3">better-sqlite3</a> 这个库，它是 Node.js 中速度最快、最简单的 SQLite 库，在 Nestjs 里也是支持用 TypeORM 来基于这个库操作 SQLite 。</p>
<h3 id="better-sqlite3-的产物"><a tabindex="-1" href="#better-sqlite3-的产物"><span></span></a>better-sqlite3 的产物</h3>
<p>和普通的依赖包直接引入 dist 产物开箱即用不一样，它还需要编译一次原生绑定文件，默认情况下，它会尝试在安装时自动构建原生模块，对比本地在 node_modules 里的目录文件，和 npmjs 上的发布文件列表，会发现线上的发布版少了 <code>better_sqlite3.node</code> 这个文件，提示的报错信息也是少了这个文件。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/02/202502161617227.jpg?x-oss-process=image/interlace,1" alt="本地在 node_modules 里的 better-sqlite3 目录文件">
<img src="https://cdn.chengpeiquan.com/img/2025/02/202502161617228.jpg?x-oss-process=image/interlace,1" alt="npmjs 上的 better-sqlite3 发布文件列表">
<h3 id="构建脚本解析"><a tabindex="-1" href="#构建脚本解析"><span></span></a>构建脚本解析</h3>
<p>在 better-sqlite3 的 <a href="https://github.com/WiseLibs/better-sqlite3/blob/v11.7.0/package.json">package.json</a> 里，可以看到 <code>install</code> 脚本就是执行这个安装后编译 Node.js 模块的操作。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "scripts"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "install"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"prebuild-install || node-gyp rebuild --release"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "build-release"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"node-gyp rebuild --release"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "build-debug"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"node-gyp rebuild --debug"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "rebuild-release"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"npm run lzz &#x26;&#x26; npm run build-release"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "rebuild-debug"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"npm run lzz &#x26;&#x26; npm run build-debug"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "test"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"mocha --exit --slow=75 --timeout=5000"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "benchmark"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"node benchmark"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "download"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"bash ./deps/download.sh"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "lzz"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"lzz -hx hpp -sx cpp -k BETTER_SQLITE3 -d -hl -sl -e ./src/better_sqlite3.lzz"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>这个 npm <code>install</code> 脚本，是 npm 的 <a href="https://docs.npmjs.com/cli/v8/using-npm/scripts#npm-install">生命周期</a> 之一，当执行 npm install 时触发（其它包管理器如 pnpm install 也会触发）。如果 npm 包的根目录下有一个名为 binding.gyp 的文件，当没有自定义 install 或 preinstall 脚本时， npm 将默认使用 <a href="https://github.com/nodejs/node-gyp">node-gyp</a> rebuild 命令对 binding.gyp 进行编译。</p>
<blockquote>
<p>node-gyp 是 Node.js 官方提供的跨平台命令行工具，用于编译 Node.js 的原生插件模块。</p>
</blockquote>
<p>可以看到 better-sqlite3 的根目录下，也是存在一个 <a href="https://github.com/WiseLibs/better-sqlite3/blob/v11.7.0/binding.gyp">binding.gyp</a> 文件，所以在 install 依赖的时候，better-sqlite3 会尝试使用 <code>prebuild-install</code> 来下载已编译好的二进制文件，如果没有找到匹配的文件，则会退回使用 <code>node-gyp rebuild --release</code> 来手动编译源代码。</p>
<p>better-sqlite3 这行 install 命令的意思是：</p>
<ol>
<li>
<p>prebuild-install：</p>
<ul>
<li>这个命令会首先检查是否有已经构建好的二进制文件（预编译的二进制文件）。 它是由 <a href="https://github.com/prebuild/prebuild-install">prebuild-install</a> 这个工具提供的，用来自动下载已编译好的二进制文件，避免在安装时重新编译原生模块，这个步骤是为了加速安装过程，并且能够在大多数情况下避免编译原生代码，尤其是针对不同平台的预编译文件。</li>
<li>prebuild-install 会检查与当前 Node.js 版本和操作系统匹配的预编译包，如果找到了，则直接使用。</li>
</ul>
</li>
<li>
<p>node-gyp rebuild --release：</p>
<ul>
<li>如果没有找到匹配的预编译文件（比如第一次安装时或没有适用的二进制文件），则会执行 node-gyp rebuild，这会尝试从源代码编译原生模块。</li>
<li>node-gyp 是一个用于编译 Node.js 本地模块的工具，它会根据模块中的 C++ 源代码生成并编译 .node 文件。</li>
<li>--release 参数表示以发布模式编译（优化后的版本），而不是调试模式。</li>
</ul>
</li>
</ol>
<p>但以上虽然是理论上的预期方案和兜底方案，但不知道为什么在 CI 机器上居然都没有执行成功，导致最后缺少了这个编译好的二进制文件。</p>
<h2 id="解决思路"><a tabindex="-1" href="#解决思路"><span></span></a>解决思路</h2>
<p>既然确认问题就是因为少了这个编译的 Node.js 模块，那么可以尝试手动 build 一下，主动生成 <code>better_sqlite3.node</code> 文件。</p>
<p>因此在 Dockerfile 里，通过 <code>pnpm i</code> 之类安装项目依赖这一步的后面，添加如下代码：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-dockerfile"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># Dockerfile</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#569CD6">RUN</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> apk add --update --no-cache python3 build-base gcc &#x26;&#x26; ln -sf /usr/bin/python3 /usr/bin/python</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#569CD6">RUN</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> cd node_modules/better-sqlite3 &#x26;&#x26; pnpm build-release</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># ...</span></span></code></pre>
<p>手动安装编译需要的依赖，并手动执行 better-sqlite3 的构建脚本，主动生成运行程序需要的 <code>better_sqlite3.node</code> 文件，然后就一切恢复正常了！</p>
<h2 id="感慨一下"><a tabindex="-1" href="#感慨一下"><span></span></a>感慨一下</h2>
<p>之前 CI 构建没问题的时候是基于 Ubuntu 22.04.5 ，现在构建失败的时候是基于 Ubuntu 24.04.1 ，只能说 CI 机器的新系统环境少了一些预装的依赖，导致 node-gyp build 没有执行成功。</p>
<p>这次自己的主服务器迁移是从旧机器的 CentOS 7 迁移到新机器的 Debian 12 ，在选择 Debian 之前，还一度先装了 Ubuntu 24 ，然后看到一些建议说作为主服务器还是稳定优先，建议用 Debian ，所以就重新装了系统。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/02/202502161727289.jpg?x-oss-process=image/interlace,1" alt="来自大佬的踩坑经验建议">
<p>没想到话音刚落就在 CI 机器踩了 Ubuntu 的坑，害，对 Linux 这些系统版本不太熟，还是得多多学习呀。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[本色十年]]></title>
            <link>https://chengpeiquan.com/article/true-colors-a-decade</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/true-colors-a-decade</guid>
            <pubDate>Mon, 27 Jan 2025 18:45:58 GMT</pubDate>
            <content:encoded><![CDATA[<p>本来这篇文章的标题按惯例应该是《年终总结：2024 年的一些回顾和 2025 年的一些小规划》 ，但 2024 年刚好也是我开始独立博客的第十年，想顺便回顾一下十年时间自己经历和变化，所以换了这个不起眼的标题哈哈哈。</p>
<h2 id="想了两个月的标题"><a tabindex="-1" href="#想了两个月的标题"><span></span></a>想了两个月的标题</h2>
<p>最初也想过用《独立博客的十年》或者是一些其它类似的就事论事的标题，但总觉得不够好，后来想到这十年时间刚好覆盖了我从一个产品运营到前端开发工程师的转变、从大厂光鲜到创业公司更好玩的从容、从广州去深圳又回到广州的生活见识，尽管工作和生活经历了很多变化，但对我个人的内心深处来说，似乎没有受到过多的影响，始终知道自己喜欢的是什么，不会因为外界的干扰而改变。</p>
<p>所以最后脑海里总是停留在《英雄本色》这部电影里，不论是它的中文名，还是英文名《A Better Tomorrow》，或者是它的主题曲《当年情》，都感觉很符合我想要的那种感觉，所以最终才决定用《本色十年》这个标题。</p>
<h2 id="我所热爱的事情"><a tabindex="-1" href="#我所热爱的事情"><span></span></a>我所热爱的事情</h2>
<p>随着年龄的增长，我越来越觉得所谓的本色，就是一个人内心深处最真实的样子，不会因为外界的干扰而改变，也不会因为时间的流逝而改变。</p>
<p>十年前，也就是 2014 年，我的好友吴庸吴老师在 <a href="https://guangne.com">他的博客</a> 上给我加了一个友情链接，他给我配的文案是：</p>
<blockquote>
<p>诗人、贝斯手、出色的厨子、编辑、切得一手好图的前端、曾经放荡过的旅行家</p>
</blockquote>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501262034446.jpg?x-oss-process=image/interlace,1" alt="吴老师博客上的友情链接">
<p>十年后，2024 年，这个文案还在他博客上挂着，挂了整整十年。</p>
<p>再看看我在 2023 年刚入职现在这家公司的时候（<a href="https://www.fnnas.com?utm_source=chengpeiquan.com">飞牛 fnOS</a>），在公司同学录里的自我介绍：</p>
<blockquote>
<p>纹了一条花臂，钟爱 Blackwork Tattoo 风格，第一个文身是我的琴；</p>
<p>养了三只猫，从 2016 年到现在，超粘人，喜欢抱着我的花臂睡觉；</p>
<p>自己跟自己玩的贝斯手，常用五弦的 MusicMan Neck-Through Bass；</p>
<p>从 2018 年留长发至今，已过肩快及腰，喜欢听摇滚乐 / 新金属 / 核；</p>
<p>家庭主厨，小红书的潮汕美食博主 <a href="https://github.com/chengpeiquan/cooking-cookbook">@底迪</a> ，擅长粤菜和潮汕菜。</p>
</blockquote>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501262043504.png?x-oss-process=image/interlace,1" alt="在公司同学录里的照片">
<p>虽然有点变化，但不多，什么都可以变，但热爱的东西不变。</p>
<h2 id="稳定了-15-个世纪的性格"><a tabindex="-1" href="#稳定了-15-个世纪的性格"><span></span></a>稳定了 1/5 个世纪的性格</h2>
<p>情绪和性格方面，这十年过得还算乐观，依然是个内向的人，依然独来独往，至于优点和缺点，好像也是维持了至少十年前的状态没啥变化。</p>
<p>优点嘛，想了想，比如：会做饭、喜欢做家务、能坚持文字阅读、情绪还算稳定、做事还算细心…… 都是一些独处的能力？好像也都是一些只要是个人都可以学会的东西……</p>
<p>缺点倒是挺多的，比如：不会抽烟、不会喝酒、不会打牌、不会打麻将、不会桌游、不会唱歌、不会打篮球、不会踢足球、不会炒股、不爱八卦、不会开摩托、不会骑电动车、不会开车…… 相对于新时代对一个普通人的要求，我好像什么都不会，可以说社交方面还真的就蛮需要这些技能的。</p>
<p>要学的话貌似也不难，但主要的阻力是自己不愿意，因为做自己不喜欢的事情很痛苦，就拿开车来说，因为从小家里很穷，出行只有自行车，后来有机会坐车的时候都是从潮州开到广州的大巴车，每次都几乎坐到吐，很害怕车的味道，打车有时候也会遇到那种味道，说不上来是什么味，很难受，心理阴影面积很大，所以一直到现在，我都不喜欢坐车，每次坐进车里还没开就会有一种心理排斥。</p>
<p>洪金宝在《奇谋妙计五福星》里的这段台词，简直就是为我量身定做，笑死。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501261745925.jpg?x-oss-process=image/interlace,1" alt="《奇谋妙计五福星》">
<h2 id="做自己喜欢的工作"><a tabindex="-1" href="#做自己喜欢的工作"><span></span></a>做自己喜欢的工作</h2>
<p>很多人转码农都是基于 “混口饭吃” ，说直白点就是趁年轻多赚点，仅此而已，但我是在考虑很久尝试很久确定自己是真的喜欢才转行的。</p>
<p>在做产品运营的时候，最早是为了在拿不到排期的时候能解决自己的需求上线而尝试自己实现，写着写着感觉做前端挺有意思的，又从前端慢慢接触到其它更多的领域，读了很多计算机大佬的故事，并且也看着很多前辈都是五六十岁还在写代码，感受到如果真的喜欢，这就是一个能玩一辈子的事情，计算机的世界太广阔了，想怎么玩都行。</p>
<p>最重要的是：这一行很适合我这种独来独往的内向人士，不像以前要出差、要去接触各种玩家、媒体，反正只要自己乐意，可以从起床直接写代码写到睡觉，不用跟什么人打交道！</p>
<p>我在知乎上回答过两个关于职业咨询的问题，有几段话虽然是对提问者说的话，但实际上也是在人生十字路口的时候会对自己说的话。</p>
<p>一个是关于是否要转岗的：</p>
<blockquote>
<p>在 “大转岗” 这个事情上面，单纯的喜欢是远远不够的，如果想在某个岗位走的更深更远，靠着一份 “喜欢” ，是支撑不了你很多年的，你至少需要上升到 “热爱” 这个层次。</p>
<p>我这里的 “大转岗” 是指 “产品转运营” / “运营转技术” 这种直接脱离原来核心能力的转岗； “小转岗” 一般是 “社区运营转直播运营” / “内容运营转新媒体运营” 这种原来的经验还可以大幅度复用的转岗。</p>
<p>“小转岗” 很正常，但是 “大转岗” ，大部分人其实都不会有很多次大转岗的机会，因为：虽然说种一棵树最好时间是十年前，其次是现在，但是这棵树要从树苗长成大树，它是需要时间的，如果没有足够的热爱去支撑你不断提升自己，那么很可能两三年就觉得又想换个岗位做一下，等到你毕业 10 年了，人家在那个岗位上已经是个 10 年经验的大佬，而你在当前的岗位，可能依然是一个只有 2、3 年经验的中级专员或工程师。</p>
<p>就像我喜欢某类型的电影，我可能就是那段时间觉得很喜欢而已（曾经漫威电影必看，到现在压根不看了）；但是我热爱的事情，比如摇滚乐、下厨、养猫，这些事情能够让我从十几岁到现在，还是十年如一日的保持着高度的热情。</p>
<p>—— <a href="https://www.zhihu.com/question/492442022/answer/2172980686">领导问我（目前任职前端）愿不愿意转产品?</a></p>
</blockquote>
<p>另一个是如何选择适合自己的公司：</p>
<blockquote>
<p>这种工作内容拖久了，实际上对你下一份工作所需要的经验沉淀、业绩沉淀，起不到什么帮助，工作越久，需要的工作经验是深耕，而不是广而不精。</p>
<p>目前你还有一个优势是，已经回到了家乡，哪怕今年疫情影响工作比较难找，但是家在身边，总归比其他人能撑得住，我认真建议你先别着急接一些奇奇怪怪的 Offer ，好好考虑一下自己的兴趣和未来的发展方向。</p>
<p>做自己喜欢的事情是最好，哪怕有时加班到半夜，也会是一种目标接近完成的兴奋感，而不是说好烦啊怎么还没搞完我不想上班了的丧。</p>
<p>—— <a href="https://www.zhihu.com/question/432129856/answer/1597050545">网易外包岗和小公司的正式员工该选哪个？</a></p>
</blockquote>
<p>特别是那句 “哪怕有时加班到半夜，也会是一种目标接近完成的兴奋感” ，最近一年在狂赶 fnOS 的需求时，总会在开发完的时候冒出来和我击掌。</p>
<h2 id="影响过我的人"><a tabindex="-1" href="#影响过我的人"><span></span></a>影响过我的人</h2>
<p>这十年来影响过我帮助过我的人很多，展示一个 Acknowledgement 在这里肯定放不下哈哈哈哈，我主要单独提一下对我在 “入门、成长、坚持” 这三个阶段影响比较大的人，没有提及的大佬们请不要介意，我一样心存感激！</p>
<h3 id="入门阶段"><a tabindex="-1" href="#入门阶段"><span></span></a>入门阶段</h3>
<p>“入门” 阶段的影响力应该属于初代淫贼三人组…… （后面不同时期有不同的淫贼 N 人组… ）</p>
<blockquote>
<p>插个词语释义： “淫贼” 是我对好友们的昵称，代表这个人人品端正、性格随和、乐观有趣、落落大方、有自己的独立人格、开得起玩笑、不会过于严肃、在一定程度上聊得来，是一个非常褒义的词语哈哈哈！</p>
</blockquote>
<p>三人组分别是：产品大佬吴庸吴老师、技术大佬振权（网名 phpbug ）和家辉（是的，真的姓张！）。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501272352617.jpg?x-oss-process=image/interlace,1" alt="2022 年离开深圳前和吴老师的合照">
<p>2014 年那会因为一些项目合作，和他们仨对接很频繁，也因为他们当时都有自己的独立博客，在他们的影响下我也尝试自己搞了起来。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501252336975.jpg?x-oss-process=image/interlace,1" alt="和吴老师提起准备十周年">
<p>在此之前完全没搞过自己的网站哈哈哈哈，也是第一次购买了自己的域名，学着很多技术大佬那样，实名制走江湖（像：阮一峰 ruanyifeng.com 、张鑫旭 zhangxinxu.com ），所以也用了自己的姓名拼音注册了域名，幸亏当时用了自己的名字，不然这些年各种中二的网名改来改去都不知道该叫什么了……</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501252249524.jpg?x-oss-process=image/interlace,1" alt="十年前注册的两个域名">
<p>还记得最早是用新浪的 SAE 托管的，后来越来越慢，而且免费用户极度不友好，就逐步迁移到阿里云用到现在（在 2018 年迁移后的第一篇博客 《 <a href="https://chengpeiquan.com/article/hello-world">世界，您好！</a> 》 有说过这个事情，刚看了一下，当时竟然还是用 Windows 做的服务…… 不堪回首），说来这次迁移可以说是绝对正确的选择，现在工作的服务也都是阿里云的，契合度 100% 。</p>
<h3 id="成长阶段"><a tabindex="-1" href="#成长阶段"><span></span></a>成长阶段</h3>
<p>在成长阶段里，前端大佬丰神对我的影响很大，除了请教过他不少问题外，他在我刚起步的时候对我说过一句话印象特别深刻，那就是 “不要只学会实现功能，还要了解实现原理” 。</p>
<p>那个时候我刚好处于 ”想实现 A 功能，就去搜包含 A 功能的 demo ，改改代码放到自己的网页上跑起来“ 的阶段，功能实现是实现了，但不知道为什么就实现了，所谓的代码能跑就行。</p>
<p>这句话在自学的过程中对我影响很大，当了解了实现原理之后，就会懂得如何举一反三去做更多的东西！哪怕没有亲自写过的也能知道个大概，以后遇到类似功能也有印象应该往哪个方向去查资料。</p>
<p>另外还有小毅 <a href="https://github.com/chawyehsu">@chawyehsu</a> ，当我还在用 jQuery 写 HTML 页面的时候，跟我分享了 Vue.js ，也就从那个时候开始慢慢知道了还有 Node.js 、 Webpack 等前端工程化的一些东西，以及来自 React / Vue 在当年完全没接触过的全新开发模式，还有不知道从哪年开始被他影响了开始在 GitHub 上活跃，在开源上真的学到了很多在公司里学不到的东西！（Btw: 他在 GitHub 也很活跃，熟悉很多开发语言，目前休息 ing ，年后有公司 OR 猎头挖人的话可以联系聊聊！）。</p>
<h3 id="坚持阶段"><a tabindex="-1" href="#坚持阶段"><span></span></a>坚持阶段</h3>
<p>这一点我要感谢从小影响我长大的黄家驹先生和 Beyond 乐队，他们的歌给人努力、乐观、坚强的感染力，并且人生真的没有污点、一直言行一致地传达着积极向上的精神。</p>
<p>就像《Beyond 日记之莫欺少年穷》的这个片段（右下角可以先暂停 BGM 再看）。</p>
<video src="https://cdn.chengpeiquan.com/video/live/beyond/beyond-s-diary-ending-part.mp4" poster="https://cdn.chengpeiquan.com/img/2025/01/202501272053285.jpg?x-oss-process=image/interlace,1" title="《Beyond 日记之莫欺少年穷》" controls preload="metadata" class="w-full aspect-video rounded-lg"><p><a href="https://cdn.chengpeiquan.com/video/live/beyond/beyond-s-diary-ending-part.mp4">https://cdn.chengpeiquan.com/video/live/beyond/beyond-s-diary-ending-part.mp4</a><a href="https://cdn.chengpeiquan.com/img/2025/01/202501272053285.jpg?x-oss-process=image/interlace,1">https://cdn.chengpeiquan.com/img/2025/01/202501272053285.jpg?x-oss-process=image/interlace,1</a>《Beyond 日记之莫欺少年穷》</p></video>
<p>还有之前某天在凌晨三点多的时候，想起小时候看过的一个香港广告《生有限 活无限》，凭着记忆里的画面关键词搜了出来，竟然是 2000 年拍的，可以说是最喜欢的一个广告片，整整 24 年都没有忘记里面的画面和文案。</p>
<video src="https://cdn.chengpeiquan.com/video/live/beyond/life-is-limited-but-life-is-unlimited.mp4" poster="https://cdn.chengpeiquan.com/img/2025/01/202501280256303.jpg?x-oss-process=image/interlace,1" title="《生有限 活无限》" controls preload="metadata" class="w-full aspect-video rounded-lg"><p><a href="https://cdn.chengpeiquan.com/video/live/beyond/life-is-limited-but-life-is-unlimited.mp4">https://cdn.chengpeiquan.com/video/live/beyond/life-is-limited-but-life-is-unlimited.mp4</a><a href="https://cdn.chengpeiquan.com/img/2025/01/202501280256303.jpg?x-oss-process=image/interlace,1">https://cdn.chengpeiquan.com/img/2025/01/202501280256303.jpg?x-oss-process=image/interlace,1</a>《生有限 活无限》</p></video>
<h2 id="开源社区"><a tabindex="-1" href="#开源社区"><span></span></a>开源社区</h2>
<p>回来讲讲我的 2024 年，虽然这一年很忙，但还是偶尔在 GitHub 上提交一些有的没的，毕竟之前开源的项目也有一些用户反馈，时不时跟进一下，另外主要就是对博客做了一次改版，这一波也是贡献了不少活跃度在里面（截图生成自 <a href="https://github-contributions.vercel.app/">GitHub Contributions</a> ）。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501252100139.jpg?x-oss-process=image/interlace,1" alt="这两年在 GitHub 的活跃情况">
<p>前段时间还在博客上线了一个 <a href="https://chengpeiquan.com/projects">开源项目</a> 的栏目，记录了一些由我创建或维护的项目，虽然没有大型项目，但有一些教程或者工具的受欢迎程度还可以，如果觉得不错，欢迎点个 Star 支持一下！</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501280240345.jpg?x-oss-process=image/interlace,1" alt="我的开源项目">
<h2 id="主力项目"><a tabindex="-1" href="#主力项目"><span></span></a>主力项目</h2>
<p>其他的事情今年没什么时间搞，主力还是在开发公司的 <a href="https://www.fnnas.com?utm_source=chengpeiquan.com">飞牛 fnOS</a> 的 Web 生态，可能很多朋友在公测期间就已经用上了，我家里的 NAS 也是装着我们的系统，用自己开发的作品影响着自己的生活！</p>
<p>如果不了解 NAS ，也可以看我之前写的《 <a href="https://chengpeiquan.com/article/my-custom-nas">千元预算组装入门 NAS 设备 分享 NAS 的硬件基础知识</a> 》 一文。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501070018282.jpg?x-oss-process=image/interlace,1" alt="飞牛 NAS 界面">
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501070018283.jpg?x-oss-process=image/interlace,1" alt="飞牛影视">
<p>刚好放假前用 <a href="https://github.com/casperdcl/git-fame">git-fame</a> 跑了一下代码贡献度，发现我竟然是贡献度最高的，有点惊喜啊哈哈！不过作为 Core Team 的第一批成员，确实参与到了很多需求里，也学到了很多东西，感谢团队！</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501272311942.jpg?x-oss-process=image/interlace,1" alt="核心仓库的代码贡献">
<p>这个产品 2025 年会正式上线，到时候欢迎大家来体验！</p>
<p>关于 2024 年和独立博客的十周年回顾，就写到这里吧，祝大家新年快乐！</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[记录一次 ERR_INCOMPLETE_CHUNKED_ENCODING 的问题排查]]></title>
            <link>https://chengpeiquan.com/article/nginx-incomplete-chunked-encoding-error</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/nginx-incomplete-chunked-encoding-error</guid>
            <pubDate>Mon, 25 Nov 2024 15:58:02 GMT</pubDate>
            <content:encoded><![CDATA[<p>最近博客改版也顺便改了部署方式，页面访问也检查了重定向配置等等，看起来似乎没什么问题，但还是收到了一个反馈 RSS 订阅源报错的情况（ issue 见 <a href="https://github.com/chengpeiquan/chengpeiquan.com/issues/370">#370</a> ，订阅源见 <a href="https://chengpeiquan.com/feed.xml">feed.xml</a> ，感谢 <a href="https://github.com/AsanZhang">@AsanZhang</a> 的反馈 ）。</p>
<p>反馈在 RSS 聚合软件里提示订阅报错了，我自己也尝试了确实不行，奇了怪了！</p>
<img src="https://cdn.chengpeiquan.com/img/2024/11/202411260041359.jpg?x-oss-process=image/interlace,1" alt="在 RSS 聚合软件里提示订阅报错了">
<h2 id="报错截图和信息"><a tabindex="-1" href="#报错截图和信息"><span></span></a>报错截图和信息</h2>
<p>在浏览器直接访问 XML ，发现 Network 里 Failed 了，控制台还报了个错误信息：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">net::ERR_INCOMPLETE_CHUNKED_ENCODING</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 200</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (OK)</span></span></code></pre>
<p>截图如下：</p>
<img src="https://cdn.chengpeiquan.com/img/2024/11/202411260038604.jpg?x-oss-process=image/interlace,1" alt="请求状态">
<img src="https://cdn.chengpeiquan.com/img/2024/11/202411260038606.jpg?x-oss-process=image/interlace,1" alt="控制台的错误信息">
<p>这个报错有点眼熟啊！想起前段时间给博客加搜索功能的时候，一开始想做全文搜索，结果部署后也遇到类似的报错，本地 build 完预览没事，线上就跪了。</p>
<img src="https://cdn.chengpeiquan.com/img/2024/11/202411261313239.jpg?x-oss-process=image/interlace,1" alt="之前做搜索的时候也遇到过类似错误">
<p>但那次因为是把所有文章都处理到一个 JSON 文件里，但因为文章里有很多代码块等内容，引起问题的原因比较多，例如可能破坏了数据结构、文件本身也很大，所以做搜索的时候最后决定去掉全文，改成了只搜标题，解决了当时遇到的问题，但没想到在 RSS 这里还是遇到类似的情况了。</p>
<p>那会还在本地 Docker 部署对比了，但本地也正常，愣是没怀疑到线上多了一层 Nginx 可能是个坑。</p>
<h2 id="解决思路"><a tabindex="-1" href="#解决思路"><span></span></a>解决思路</h2>
<p>由于对 Nginx 并没有过多的深入使用，常年处于基础的转发配置阶段，所以直接请教 GPT 帮我解决。</p>
<img src="https://cdn.chengpeiquan.com/img/2024/11/202411260112210.jpg?x-oss-process=image/interlace,1" alt="ChatGPT 给我的解决思路">
<h2 id="调整-nginx-的配置"><a tabindex="-1" href="#调整-nginx-的配置"><span></span></a>调整 Nginx 的配置</h2>
<p>原来的配置是这样子，比较早期的默认配置：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-nginx"><span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">server_names_hash_bucket_size </span><span style="color:#986801;--shiki-dark:#B5CEA8">128</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">client_header_buffer_size </span><span style="color:#986801;--shiki-dark:#B5CEA8">32k</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">large_client_header_buffers </span><span style="color:#986801;--shiki-dark:#B5CEA8">4</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 32k</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">client_max_body_size </span><span style="color:#986801;--shiki-dark:#B5CEA8">50m</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">sendfile </span><span style="color:#986801;--shiki-dark:#569CD6">  on</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">tcp_nopush </span><span style="color:#986801;--shiki-dark:#569CD6">on</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">keepalive_timeout </span><span style="color:#986801;--shiki-dark:#B5CEA8">60</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">tcp_nodelay </span><span style="color:#986801;--shiki-dark:#569CD6">on</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">fastcgi_connect_timeout </span><span style="color:#986801;--shiki-dark:#B5CEA8">300</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">fastcgi_send_timeout </span><span style="color:#986801;--shiki-dark:#B5CEA8">300</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">fastcgi_read_timeout </span><span style="color:#986801;--shiki-dark:#B5CEA8">300</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">fastcgi_buffer_size </span><span style="color:#986801;--shiki-dark:#B5CEA8">64k</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">fastcgi_buffers </span><span style="color:#986801;--shiki-dark:#B5CEA8">4</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 64k</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">fastcgi_busy_buffers_size </span><span style="color:#986801;--shiki-dark:#B5CEA8">128k</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">fastcgi_temp_file_write_size </span><span style="color:#986801;--shiki-dark:#B5CEA8">256k</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span></code></pre>
<p>按照 GPT 的描述， <code>proxy_buffer_size</code> 、 <code>proxy_buffers</code> 和 <code>proxy_busy_buffers_size</code> 这些参数用于调整代理缓冲区的大小， <code>fastcgi_buffer_size</code> 、 <code>fastcgi_buffers</code> 和 <code>fastcgi_busy_buffers_size</code> 这些参数用于调整 FastCGI 缓冲区的大小，另外还建议我新增 Proxy 缓冲区相关配置。</p>
<p>打开 nginx.conf 文件，修改配置如下：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-diff"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">fastcgi_connect_timeout 300;</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">fastcgi_send_timeout 300;</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">fastcgi_read_timeout 300;</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#CE9178">-fastcgi_buffer_size 64k;</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+fastcgi_buffer_size 128k;</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#CE9178">-fastcgi_buffers 4 64k;</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+fastcgi_buffers 4 128k;</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#CE9178">-fastcgi_busy_buffers_size 128k;</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+fastcgi_busy_buffers_size 256k;</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#CE9178">-fastcgi_temp_file_write_size 256k;</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+fastcgi_temp_file_write_size 512k;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+proxy_buffer_size 128k;</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+proxy_buffers 8 128k;</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+proxy_busy_buffers_size 256k;</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+proxy_temp_file_write_size 512k;</span></span></code></pre>
<p>调整完这些配置后，重启 Nginx 服务以应用更改。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">sudo</span><span style="color:#50A14F;--shiki-dark:#CE9178"> nginx</span><span style="color:#986801;--shiki-dark:#569CD6"> -s</span><span style="color:#50A14F;--shiki-dark:#CE9178"> reload</span></span></code></pre>
<p>现在确实解决了，成功订阅！</p>
<img src="https://cdn.chengpeiquan.com/img/2024/11/202411261331139.jpg?x-oss-process=image/interlace,1" alt="成功订阅！">]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[使用 remark-directive 为 Unifiedjs 提供 Markdown 视频语法的解析]]></title>
            <link>https://chengpeiquan.com/article/use-remark-directive-to-provide-markdown-video-syntax-parsing-for-unifiedjs</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/use-remark-directive-to-provide-markdown-video-syntax-parsing-for-unifiedjs</guid>
            <pubDate>Sat, 09 Nov 2024 17:34:05 GMT</pubDate>
            <content:encoded><![CDATA[<p>最近对博客进行了一次技术栈迁移，其中对 Markdown 的解析渲染支持也从 <a href="https://github.com/markdown-it">Markdown-it</a> 系列迁移至 <a href="https://github.com/unifiedjs">Unifiedjs</a> 系列，在 Unified 的工作流程里，又包含了处理 Markdown 的 <a href="https://github.com/remarkjs">Remarkjs</a> 系列以及处理 HTML 的 <a href="https://github.com/rehypejs">Rehypejs</a> 系列。</p>
<p>在博客里， Markdown Parser 的整个工作流程都是自己管理的，包括不同结果的输出，例如：提供给 RSS 订阅用的 HTML ，提供给列表和搜索用的 Metadata ，以及提供给详情页作为 React 组件渲染内容用的 JSX ，这些过程并不算复杂，事实上进展确实是很顺利，但是在我以为即将大功告成之际，突然发现渲染出来的内容少了一个东西：我的视频呢？</p>
<h2 id="改版前的表现"><a tabindex="-1" href="#改版前的表现"><span></span></a>改版前的表现</h2>
<p>改版之前是使用 Markdown-it 作为技术栈， Markdown 代码与 HTML 代码的相处非常和谐，对于没有 Markdown 原生语法支持的 HTML 标签，都可以直接编写 HTML 代码进行渲染，内嵌视频最初就是这样子实现的。</p>
<p>像这样，在 Markdown 里直接编写 HTML 代码，即可直接输出 HTML 。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-md"><span class="line"><span style="color:#383A42;--shiki-dark:#808080">&#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">video</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> </span></span>
<span class="line"><span style="color:#986801;--shiki-dark:#9CDCFE">  src</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4"</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> </span></span>
<span class="line"><span style="color:#986801;--shiki-dark:#9CDCFE">  poster</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1"</span></span>
<span class="line"><span style="color:#986801;--shiki-dark:#9CDCFE">  title</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"山景房里的三只猫"</span></span>
<span class="line"><span style="color:#986801;--shiki-dark:#9CDCFE">  controls</span></span>
<span class="line"><span style="color:#986801;--shiki-dark:#9CDCFE">  preload</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"auto"</span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#F44747">/</span><span style="color:#383A42;--shiki-dark:#808080">></span></span></code></pre>
<p>但改版后，原本应该渲染为视频的地方，都只剩下一个 <code>&#x3C;p>&#x3C;/p></code> 标签，很明显是在 Markdown 代码转换过程中被过滤了。</p>
<h2 id="理解-unified-的工作流程"><a tabindex="-1" href="#理解-unified-的工作流程"><span></span></a>理解 Unified 的工作流程</h2>
<p>像我这种 Parser 流程比较长，中间处理环节还是动态变化的情况，很怕这些奇怪的问题，但还好 Unified 的设计非常清晰，先了解一下实现原理，更方便找到问题的原因。上面提到了 Unified 包含了 Remark 和 Rehype 两个系列的工作流，因为在 Unified 生态的工作过程中，都是基于 AST 语法树工作，可以简单地理解为：</p>
<ol>
<li>先由 Remark 负责把 Markdown 文件的内容转为 MDAST ，在这个过程中所有代码都是围绕 Markdown 工作</li>
<li>再通过中间插件 <a href="https://github.com/remarkjs/remark-rehype">remark-rehype</a> ，把 MDAST 转为 HAST</li>
<li>最终由 Rehype 处理 HAST ，自此阶段开始，所有工作都是围绕 HTML 展开，最终输出什么样的结果也是在这个阶段处理</li>
</ol>
<p>以上流程可以反过来，也就是先处理 HTML 再还原为 Markdown ，如果是这种流程，中间插件需要更换为 <a href="https://github.com/rehypejs/rehype-remark">rehype-remark</a> 。</p>
<blockquote>
<p>名词解释：</p>
<p>MDAST —— Markdown Abstract Syntax Tree ， Markdown 抽象语法树</p>
<p>HAST —— Hypertext Abstract Syntax Tree ，超文本抽象语法树</p>
</blockquote>
<h2 id="问题的排查"><a tabindex="-1" href="#问题的排查"><span></span></a>问题的排查</h2>
<p>了解了工作流程，就可以分三个阶段排查问题了，要么就是在 Remark 环节把 HTML 代码屏蔽了，要么就是 Rehype 环节有问题，要么就是中间的 AST 转换抛弃了这部分代码。</p>
<p>此时 Parser 里的处理器插件是这么启用的：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> processor</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> unified</span><span style="color:#383A42;--shiki-dark:#D4D4D4">()</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  .</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">use</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkPlugins</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// Markdown to MDAST</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  .</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">use</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkRehypePlugins</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// MDAST to HAST</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  .</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">use</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">rehypePlugins</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// HAST to HTML</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  .</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">use</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">reactPlugins</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// HTML to JSX</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> file</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#A626A4;--shiki-dark:#C586C0"> await</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> processor</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">process</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">markdown</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span></code></pre>
<p>这里的每一个 Plugins 变量都是一个数组，会根据我的构建场景动态启用插件（相关源码见：<a href="https://github.com/chengpeiquan/chengpeiquan.com/blob/main/src/core/parser/index.ts">core/parser</a> ），例如：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> remarkParse</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'remark-parse'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> remarkGfm</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'remark-gfm'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> remarkStringify</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'remark-stringify'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#A626A4;--shiki-dark:#C586C0">type</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> PluggableList</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'unified'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> remarkPlugins</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> PluggableList</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkParse</span><span style="color:#383A42;--shiki-dark:#D4D4D4">], </span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// e.g. [plugin, pluginOptions]</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkGfm</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkStringify</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span></code></pre>
<p>因此先仅启用 remarkPlugins ，发现一切正常，继续启用 remarkRehypePlugins ，就出问题了， Markdown 里的 <code>&#x3C;video /></code> 标签被过滤了。</p>
<p>所以我在 remark-rehype 的文档里找到了关于 HTML 标签过滤的说明：</p>
<blockquote>
<p>因为在 markdown 中支持 HTML 是一项繁重的任务（性能和包大小） ，而且并不总是需要的，要同时使用两者，您还必须配置 <code>allowDangerousHtml: true</code> 选项。 —— 详见 <a href="https://github.com/remarkjs/remark-rehype?tab=readme-ov-file#when-should-i-use-this">When should I use this?</a></p>
</blockquote>
<h2 id="解决方案"><a tabindex="-1" href="#解决方案"><span></span></a>解决方案</h2>
<p>原因被定位到了就很好解决，目前是找到了这些解决方案，可以根据需要处理。</p>
<h3 id="开启-allowdangeroushtml"><a tabindex="-1" href="#开启-allowdangeroushtml"><span></span></a>开启 allowDangerousHtml</h3>
<p>根据 <a href="https://github.com/remarkjs/remark-rehype">remark-rehype</a> 的文档，仅需开启该选项即可支持将 Markdown 里的 HTML 代码作为半标准节点嵌入 HAST 中 <code>raw</code> 。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> remarkRehype</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'remark-rehype'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#A626A4;--shiki-dark:#C586C0">type</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> PluggableList</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'unified'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> remarkRehypePlugins</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> PluggableList</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkRehype</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">allowDangerousHtml</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#986801;--shiki-dark:#569CD6"> true</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> }],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span></code></pre>
<blockquote>
<p>注意：除了该插件需要开启该选项之外，像我的博客还使用了 <a href="https://github.com/rehypejs/rehype/tree/main/packages/rehype-stringify">rehype-stringify</a> 插件，它也需要一起开启该选项。</p>
</blockquote>
<p>由于我还使用了 <a href="https://github.com/rehypejs/rehype-sanitize">rehype-sanitize</a> 用于对 HTML 内容的清理，因此仅开启该选项在我的博客里并不能直接达到目的，还要在 Sanitize 进行放行，并且平时写 React 组件的习惯上，我对 <a href="https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html">dangerouslySetInnerHTML</a> 的使用非常克制，有一些代码洁癖让我不喜欢这个方案，因此我放弃了它。</p>
<h3 id="将-raw-html-转为-hast"><a tabindex="-1" href="#将-raw-html-转为-hast"><span></span></a>将 Raw HTML 转为 HAST</h3>
<p>在 <a href="https://github.com/remarkjs/remark-rehype">remark-rehype</a> 的文档里，描述 <code>allowDangerousHtml</code> 部分提及到了另外一个插件： <a href="https://github.com/rehypejs/rehype-raw">rehype-raw</a> 。</p>
<p>这个插件很适合希望支持渲染嵌入在 Markdown 里的 HTML（需要传递 <code>allowDangerousHtml: true</code> 给 remark-rehype ）,它可以获取 Markdown 里的 HTML 字符串并将它们作为实际节点包含到 HAST 中。</p>
<p>在开启 allowDangerousHtml 选项时， Markdown 里的 HTML 代码仅作为半标准节点嵌入 HAST 中 <code>raw</code> 属性，但配合这个插件，可以将原始的 HTML 字符串解析为标准的 HAST 节点。</p>
<p>处理过程需要依赖一个完整的 HTML 解析器（详见 <a href="https://github.com/inikulin/parse5">parse5</a> ），它将完全按照浏览器解析的方式重新创建抽象语法树，同时保持原始数据和位置信息完好无损。</p>
<p>注意在使用过程中的插件顺序：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> remarkRehype</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'remark-rehype'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> rehypeStringify</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'rehype-stringify'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> rehypeRaw</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'rehype-raw'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#A626A4;--shiki-dark:#C586C0">type</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> PluggableList</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'unified'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> remarkRehypePlugins</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> PluggableList</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkRehype</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">allowDangerousHtml</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#986801;--shiki-dark:#569CD6"> true</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> }],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> rehypePlugins</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> PluggableList</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">rehypeStringify</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">allowDangerousHtml</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#986801;--shiki-dark:#569CD6"> true</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> }],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">rehypeRaw</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span></code></pre>
<p>这个方案处理过程比较繁重，但这是支持不受信任内容的唯一方法，除非类似那种内容完全由用户提交的场景，否则在内容可控的场景下，都不推荐使用这个方案。</p>
<h3 id="使用-markdown-图片语法"><a tabindex="-1" href="#使用-markdown-图片语法"><span></span></a>使用 Markdown 图片语法</h3>
<p>这是一个最轻巧的解决方案，几乎没有多余的处理成本。</p>
<p>因为我的博客文章详情页最终是通过 JSX 进行渲染（可参考 <a href="https://github.com/chengpeiquan/chengpeiquan.com/blob/main/src/components/markup/renderer.tsx">markup/renderer</a> ），因此完整的处理过程是：<code>Markdown > MDAST > HAST > HTML > JSX</code> ，在最后一个环节使用 <a href="https://github.com/rehypejs/rehype-react">rehype-react</a> 的时候，可以将 HTML 代码转换为 React 组件需要的 JSX 代码。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> rehypeReact</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, { </span><span style="color:#A626A4;--shiki-dark:#C586C0">type</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> Options</span><span style="color:#383A42;--shiki-dark:#C586C0"> as</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> RehypeReactOptions</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'rehype-react'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">a</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">img</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> './components'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">Fragment</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#383A42;--shiki-dark:#9CDCFE">jsx</span><span style="color:#383A42;--shiki-dark:#C586C0"> as</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> _jsx</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#383A42;--shiki-dark:#9CDCFE">jsxs</span><span style="color:#383A42;--shiki-dark:#C586C0"> as</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> _jsxs</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'react/jsx-runtime'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#A626A4;--shiki-dark:#C586C0">type</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> PluggableList</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'unified'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> components</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  a</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// e.g. `&#x3C;a />` --> Next.js `&#x3C;Link />`</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  img</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// e.g. `&#x3C;img />` --> Next.js `&#x3C;Image />`</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">} </span><span style="color:#383A42;--shiki-dark:#C586C0">as</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span><span style="color:#383A42;--shiki-dark:#C586C0"> as</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> RehypeReactOptions</span><span style="color:#383A42;--shiki-dark:#D4D4D4">[</span><span style="color:#50A14F;--shiki-dark:#CE9178">'components'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> rehypeReactOptions</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  Fragment</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  components</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// e.g. Record&#x3C;tagName, componentName></span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  ignoreInvalidStyle</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#986801;--shiki-dark:#569CD6"> true</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  jsx</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  jsxs</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  passKeys</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#986801;--shiki-dark:#569CD6"> true</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  passNode</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#986801;--shiki-dark:#569CD6"> true</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  development</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#986801;--shiki-dark:#569CD6"> false</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">} </span><span style="color:#A626A4;--shiki-dark:#C586C0">satisfies</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> RehypeReactOptions</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> reactPlugins</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> PluggableList</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [[</span><span style="color:#383A42;--shiki-dark:#9CDCFE">rehypeReact</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#383A42;--shiki-dark:#9CDCFE">rehypeReactOptions</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]]</span></span></code></pre>
<p>所以我想到了一个方案，使用 Markdown 内置的图片语法，将视频链接放在原本需要放图片链接的位置，然后在转 JSX 的过程中，判断 URL 结尾的扩展名将视频 URL 分配给 Video 组件。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-md"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">![</span><span style="color:#4078F2;--shiki-dark:#CE9178">山景房里的三只猫</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span><span style="color:#A626A4;--shiki-dark:#D4D4D4">(</span><span style="color:#A626A4;--shiki-light-text-decoration:inherit;--shiki-dark:#D4D4D4;--shiki-dark-text-decoration:underline">https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4</span><span style="color:#A626A4;--shiki-dark:#D4D4D4">)</span></span></code></pre>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// components.tsx</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> video</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#A626A4;--shiki-dark:#569CD6"> async</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">props</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#50A14F;--shiki-dark:#4EC9B0"> React</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#C18401;--shiki-dark:#4EC9B0">VideoHTMLAttributes</span><span style="color:#383A42;--shiki-dark:#D4D4D4">&#x3C;</span><span style="color:#C18401;--shiki-dark:#4EC9B0">HTMLVideoElement</span><span style="color:#383A42;--shiki-dark:#D4D4D4">>) </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  // 组件里的其它逻辑</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  // ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">  return</span><span style="color:#383A42;--shiki-dark:#808080"> &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">video</span><span style="color:#383A42;--shiki-dark:#569CD6"> {</span><span style="color:#383A42;--shiki-dark:#D4D4D4">...</span><span style="color:#383A42;--shiki-dark:#9CDCFE">props</span><span style="color:#383A42;--shiki-dark:#569CD6">}</span><span style="color:#383A42;--shiki-dark:#808080"> /></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#A626A4;--shiki-dark:#569CD6"> const</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> img</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#A626A4;--shiki-dark:#569CD6"> async</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">props</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#50A14F;--shiki-dark:#4EC9B0"> React</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#C18401;--shiki-dark:#4EC9B0">ImgHTMLAttributes</span><span style="color:#383A42;--shiki-dark:#D4D4D4">&#x3C;</span><span style="color:#C18401;--shiki-dark:#4EC9B0">HTMLImageElement</span><span style="color:#383A42;--shiki-dark:#D4D4D4">>) </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  // 判断常用的视频文件扩展名，将其转发给视频组件渲染</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">  if</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#E45649;--shiki-dark:#9CDCFE">props</span><span style="color:#383A42;--shiki-dark:#D4D4D4">?.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">src</span><span style="color:#383A42;--shiki-dark:#D4D4D4">?.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">endsWith</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#50A14F;--shiki-dark:#CE9178">'.mp4'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)) {</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">    return</span><span style="color:#383A42;--shiki-dark:#808080"> &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">video</span><span style="color:#383A42;--shiki-dark:#569CD6"> {</span><span style="color:#383A42;--shiki-dark:#D4D4D4">...</span><span style="color:#383A42;--shiki-dark:#9CDCFE">props</span><span style="color:#383A42;--shiki-dark:#569CD6">}</span><span style="color:#383A42;--shiki-dark:#808080"> /></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  // 组件里的其它逻辑</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  // ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">  return</span><span style="color:#383A42;--shiki-dark:#808080"> &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">img</span><span style="color:#383A42;--shiki-dark:#569CD6"> {</span><span style="color:#383A42;--shiki-dark:#D4D4D4">...</span><span style="color:#383A42;--shiki-dark:#9CDCFE">props</span><span style="color:#383A42;--shiki-dark:#569CD6">}</span><span style="color:#383A42;--shiki-dark:#808080"> /></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>事实上在文章详情里实现很完美，但我考虑到了 RSS 订阅源里的 HTML 代码并没有得到解决，并且这种方式无法配置视频的 <code>poster</code> 属性，所以这个方案也被我放弃了。</p>
<h3 id="使用-markdown-自定义指令"><a tabindex="-1" href="#使用-markdown-自定义指令"><span></span></a>使用 Markdown 自定义指令</h3>
<p>这个方案是在 GitHub <a href="https://github.com/orgs/remarkjs/discussions">Remarkjs Discussions</a> 里搜索时找到了几个讨论，提供了很棒的灵感！</p>
<ul>
<li><a href="https://github.com/orgs/remarkjs/discussions/1043">Embed videos in markdown? #1043</a></li>
<li><a href="https://github.com/orgs/remarkjs/discussions/957">displaying video by link in react-markdown #957</a></li>
<li><a href="https://github.com/orgs/remarkjs/discussions/557">What can remark actually do / how to display videos in markdown? #557</a></li>
<li><a href="https://github.com/orgs/remarkjs/discussions/747">How to type a custom plugin on project? #747</a></li>
</ul>
<p>Remark 提供了这方面的插件支持，仅需安装 <a href="https://github.com/remarkjs/remark-directive">remark-directive</a> 插件，这是对 <a href="https://talk.commonmark.org/t/generic-directives-plugins-syntax/444">Markdown 指令语法提案</a> 的实现（这个提案很有意思，值得阅读！），可以使用和 Markdown 十分接近的语法实现一些自定义功能。</p>
<p>看到这里的读者应该不会陌生，很多知名的静态生成器项目都支持自定义指令，例如：</p>
<ul>
<li><a href="https://docusaurus.io/zh-CN/docs/markdown-features/admonitions">告示 - Docusaurus</a></li>
<li><a href="https://vitepress.dev/zh/guide/markdown#custom-containers">自定义容器 - VitePress</a></li>
</ul>
<h2 id="最终实现方案"><a tabindex="-1" href="#最终实现方案"><span></span></a>最终实现方案</h2>
<p>简单说一下实现方案，最终是通过编写一个 Remark 插件实现自定义指令，以 <code>:::video</code> 的语法，在 Markdown 内容里配置视频的 <code>src</code> 、 <code>poster</code> 、 <code>title</code> 属性。</p>
<blockquote>
<p>源码现在维护在 <a href="https://github.com/chengpeiquan/blackwork/blob/main/packages/machine/src/plugins/remark-video.ts">@blackwork/machine/remark-video</a> ，这里贴的代码在未来可能会有变化。</p>
</blockquote>
<h3 id="所需的依赖"><a tabindex="-1" href="#所需的依赖"><span></span></a>所需的依赖</h3>
<p>先安装依赖，由于不需要在运行时使用，所以统一安装到 <code>devDependencies</code> 里。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">pnpm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> add</span><span style="color:#986801;--shiki-dark:#569CD6"> -D</span><span style="color:#50A14F;--shiki-dark:#CE9178"> remark-directive</span><span style="color:#50A14F;--shiki-dark:#CE9178"> unist-util-visit</span><span style="color:#50A14F;--shiki-dark:#CE9178"> @types/mdast</span></span></code></pre>
<p>这些插件的作用：</p>
<table>
<thead>
<tr>
<th align="center">插件</th>
<th align="center">作用</th>
<th align="center">写本文时使用的版本号</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">remark-directive</td>
<td align="center">添加对通用指令的支持</td>
<td align="center">^3.0.0</td>
</tr>
<tr>
<td align="center">unist-util-visit</td>
<td align="center">遍历 AST 语法树节点，导出了一个 <code>visit</code> 方法</td>
<td align="center">^5.0.0</td>
</tr>
<tr>
<td align="center">@types/mdast</td>
<td align="center">为 TypeScript 提供插件主要参数的类型</td>
<td align="center">^4.0.4</td>
</tr>
</tbody>
</table>
<h3 id="设计时的想法"><a tabindex="-1" href="#设计时的想法"><span></span></a>设计时的想法</h3>
<p>考虑到需要配置的参数如 <code>src</code> 和 <code>poster</code> 的 URL 都比较长，用 <code>leafDirective</code> 语法会比较难维护，因此选择了 <code>containerDirective</code> 语法，按照约定，从上往下分为三行内容，分别是 <code>src</code> 、 <code>poster</code> 以及 <code>title</code> ：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-md"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">:::video</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">https://example.com/video-src.mp4</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">https://example.com/video-poster.jpg</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">A video title</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">:::</span></span></code></pre>
<p>其他的视频播放器属性，由指令插件统一管理，因此不需要在 Markdown 里自定义配置。</p>
<h3 id="编写指令插件"><a tabindex="-1" href="#编写指令插件"><span></span></a>编写指令插件</h3>
<p>按照 README 的例子，很快就能编写一个自定义插件了，这里就不赘述具体的过程，看代码和注释即可。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#A626A4;--shiki-dark:#C586C0">type</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> Root</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'mdast'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">visit</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'unist-util-visit'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#E45649;--shiki-dark:#9CDCFE">isArray</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">isObject</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#E45649;--shiki-dark:#9CDCFE">isString</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '@bassist/utils'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// For the `src` and `poster` attributes</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">interface</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> LinkNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  type</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'link'</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  url</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> string</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  children</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">?:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span><span style="color:#383A42;--shiki-dark:#D4D4D4">[]</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">key</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#383A42;--shiki-dark:#9CDCFE">string</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// For the `title` attribute</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">interface</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> TextNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  type</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'text'</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  value</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> string</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">key</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#383A42;--shiki-dark:#9CDCFE">string</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">interface</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> VideoDirectiveNodeChildren</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  children</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#C18401;--shiki-dark:#4EC9B0">TextNode</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> |</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> LinkNode</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> |</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)[]</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">interface</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> VideoDirectiveNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  type</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'containerDirective'</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  name</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'video'</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  children</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> VideoDirectiveNodeChildren</span><span style="color:#383A42;--shiki-dark:#D4D4D4">[]</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">key</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#383A42;--shiki-dark:#9CDCFE">string</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">interface</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> HyperScriptData</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  hName</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">?:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> string</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">  hProperties</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">?:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">    [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">key</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> string</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">key</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#383A42;--shiki-dark:#9CDCFE">string</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">/**</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * With container directive</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">example</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   ;```md</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   :::video</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   src</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   poster</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   title</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   :::</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   ```</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> */</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isVideoNode</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> i</span><span style="color:#0184BC;--shiki-dark:#569CD6"> is</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> VideoDirectiveNode</span><span style="color:#A626A4;--shiki-dark:#569CD6"> =></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">  if</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">!</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">isObject</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)) </span><span style="color:#A626A4;--shiki-dark:#C586C0">return</span><span style="color:#986801;--shiki-dark:#569CD6"> false</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">  const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> children</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">?.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">children</span><span style="color:#383A42;--shiki-dark:#D4D4D4">?.[</span><span style="color:#986801;--shiki-dark:#B5CEA8">0</span><span style="color:#383A42;--shiki-dark:#D4D4D4">]?.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">children</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">  return</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">    i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">type</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> ===</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'containerDirective'</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> &#x26;&#x26;</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">    i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">name</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> ===</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'video'</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> &#x26;&#x26;</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">    isArray</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">children</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#0184BC;--shiki-dark:#D4D4D4">&#x26;&#x26;</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">    children</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">length</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> ></span><span style="color:#986801;--shiki-dark:#B5CEA8"> 0</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  )</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isLinkNode</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> i</span><span style="color:#0184BC;--shiki-dark:#569CD6"> is</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> LinkNode</span><span style="color:#A626A4;--shiki-dark:#569CD6"> =></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">  return</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isObject</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#0184BC;--shiki-dark:#D4D4D4">&#x26;&#x26;</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">type</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> ===</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'link'</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> &#x26;&#x26;</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isString</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">url</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#0184BC;--shiki-dark:#D4D4D4">&#x26;&#x26;</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> !!</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">url</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isTextNode</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> i</span><span style="color:#0184BC;--shiki-dark:#569CD6"> is</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> TextNode</span><span style="color:#A626A4;--shiki-dark:#569CD6"> =></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">  return</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isObject</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#0184BC;--shiki-dark:#D4D4D4">&#x26;&#x26;</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">type</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> ===</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'text'</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> &#x26;&#x26;</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isString</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">value</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#0184BC;--shiki-dark:#D4D4D4">&#x26;&#x26;</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> !!</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">value</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isValidChildNode</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isLinkNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#0184BC;--shiki-dark:#D4D4D4">||</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> isTextNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">/**</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * I have customized a compilation process in Markdown Parser, so not all HTML</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * codes are allowed to be rendered.</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * When Markdown is being converted to AST, many HTML tags will be discarded,</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * and the same is true for Video.</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * In order to uniformly implement custom rendering content, this plugin</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * implements the ability of `video` directive.</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * One more important thing, since rehype-sanitize is enabled, remember to</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * configure the options to allow rendering of video tags and attributes.</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">example</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   Enter the following into the markdown file:</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   ```md</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   :::video</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   https://example.com/video-src.mp4</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   https://example.com/video-poster.jpg</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   A video title</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   :::</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   ```</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   Compile and output a Video tag:</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   ```html</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   &#x3C;video</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   src="https://example.com/video-src.mp4"</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   poster="https://example.com/video-poster.jpg"</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   title="Hello World"</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   /></span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *   ```</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> *</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> * </span><span style="color:#383A42;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">@</span><span style="color:#A626A4;--shiki-light-font-style:italic;--shiki-dark:#569CD6;--shiki-dark-font-style:inherit">returns</span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> Transformer</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"> */</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#4078F2;--shiki-dark:#DCDCAA"> remarkVideo</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> () </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">  return</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">tree</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> Root</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">    // Prevents the following judgment from being inferred as never</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">    visit</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">tree</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">node</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#0184BC;--shiki-dark:#4EC9B0"> unknown</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">      if</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">!</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">isVideoNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">node</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)) </span><span style="color:#A626A4;--shiki-dark:#C586C0">return</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">      const</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#986801;--shiki-dark:#4FC1FF">srcNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#986801;--shiki-dark:#4FC1FF">posterNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#986801;--shiki-dark:#4FC1FF">titleNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">] </span><span style="color:#0184BC;--shiki-dark:#D4D4D4">=</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> node</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">children</span><span style="color:#383A42;--shiki-dark:#D4D4D4">[</span><span style="color:#986801;--shiki-dark:#B5CEA8">0</span><span style="color:#383A42;--shiki-dark:#D4D4D4">].</span><span style="color:#E45649;--shiki-dark:#9CDCFE">children</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">        .</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">map</span><span style="color:#383A42;--shiki-dark:#D4D4D4">((</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A626A4;--shiki-dark:#569CD6">=></span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">          if</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">isLinkNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)) </span><span style="color:#A626A4;--shiki-dark:#C586C0">return</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> i</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">          if</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">isTextNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)) {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">            i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">value</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">value</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">replace</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#0184BC;--shiki-dark:#D16969">/</span><span style="color:#986801;--shiki-dark:#D16969">\n</span><span style="color:#0184BC;--shiki-dark:#D16969">/</span><span style="color:#A626A4;--shiki-dark:#569CD6">g</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">''</span><span style="color:#383A42;--shiki-dark:#D4D4D4">).</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">trim</span><span style="color:#383A42;--shiki-dark:#D4D4D4">()</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">            if</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">i</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">value</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#A626A4;--shiki-dark:#C586C0">return</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> i</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">          }</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">          return</span><span style="color:#986801;--shiki-dark:#569CD6"> undefined</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">        })</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">        .</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">filter</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">isValidChildNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">      const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> src</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> srcNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">url</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">      const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> poster</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> posterNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">url</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">      const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> title</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#9CDCFE"> titleNode</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">value</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">      const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> data</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">node</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">data</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> ||</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (</span><span style="color:#383A42;--shiki-dark:#9CDCFE">node</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">data</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {})) </span><span style="color:#383A42;--shiki-dark:#C586C0">as</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> HyperScriptData</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">      data</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">hName</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'video'</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">      data</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#E45649;--shiki-dark:#9CDCFE">hProperties</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">        src</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">        poster</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">        title</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">        controls</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#986801;--shiki-dark:#569CD6"> true</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">        preload</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'auto'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">        className</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'w-full aspect-video rounded-lg'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">      }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">    })</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> remarkVideo</span></span></code></pre>
<h3 id="启用插件"><a tabindex="-1" href="#启用插件"><span></span></a>启用插件</h3>
<p>在我的博客项目里，是在 <a href="https://github.com/chengpeiquan/chengpeiquan.com/blob/main/src/core/parser/index.ts">core/parser</a> 里启用插件（也就是最终提供给 <code>unified().use()</code> 使用 ），在使用的过程中，如果启用了另外一个 <a href="https://github.com/rehypejs/rehype-sanitize">rehype-sanitize</a> 插件，还需要在该插件的选项里配置 <code>tagNames</code> 和 <code>attributes</code> 的白名单列表。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> remarkDirective</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'remark-directive'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> remarkVideo</span><span style="color:#A626A4;--shiki-dark:#C586C0"> from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> './plugins/remark-video'</span></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">import</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> { </span><span style="color:#A626A4;--shiki-dark:#C586C0">type</span><span style="color:#E45649;--shiki-dark:#9CDCFE"> PluggableList</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> } </span><span style="color:#A626A4;--shiki-dark:#C586C0">from</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'unified'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> remarkPlugins</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> PluggableList</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkParse</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkDirective</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span><span style="color:#383A42;--shiki-dark:#9CDCFE">remarkVideo</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A626A4;--shiki-dark:#569CD6">const</span><span style="color:#986801;--shiki-dark:#4FC1FF"> rehypePlugins</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">:</span><span style="color:#C18401;--shiki-dark:#4EC9B0"> PluggableList</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> =</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  // ...</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  [</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#9CDCFE">    rehypeSanitize</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">    {</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">      // No need `user-content-` prefix</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">      clobberPrefix</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ''</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">      // https://github.com/syntax-tree/hast-util-sanitize#tagnames</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">      tagNames</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#0184BC;--shiki-dark:#D4D4D4">...</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">toArray</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">defaultSchema</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#C18401;--shiki-dark:#9CDCFE">tagNames</span><span style="color:#383A42;--shiki-dark:#D4D4D4">), </span><span style="color:#50A14F;--shiki-dark:#CE9178">'video'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">      // https://github.com/syntax-tree/hast-util-sanitize#attributes</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">      attributes</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {</span></span>
<span class="line"><span style="color:#0184BC;--shiki-dark:#D4D4D4">        ...</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#383A42;--shiki-dark:#9CDCFE">defaultSchema</span><span style="color:#383A42;--shiki-dark:#D4D4D4">.</span><span style="color:#C18401;--shiki-dark:#9CDCFE">attributes</span><span style="color:#0184BC;--shiki-dark:#D4D4D4"> ||</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> {}),</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">        video</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#50A14F;--shiki-dark:#CE9178">'src'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">'poster'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">'controls'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">'preload'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">'className'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">      },</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">    },</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  ],</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  // ...</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// ...</span></span></code></pre>
<h3 id="最终结果"><a tabindex="-1" href="#最终结果"><span></span></a>最终结果</h3>
<p>这就是这段 Markdown 指令渲染出来的效果（当然，不包括下面的标题展示，那是我另外包裹了一层 <code>figure</code> 标签，详见 <a href="https://github.com/chengpeiquan/chengpeiquan.com/blob/main/src/core/parser/components/index.tsx">parser/components</a> ，在转换为 JSX 的时候处理的）。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-md"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">:::video</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">山景房里的三只猫</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">:::</span></span></code></pre>
<video src="https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4" poster="https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1" title="山景房里的三只猫" controls preload="metadata" class="w-full aspect-video rounded-lg"><p><a href="https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4">https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4</a><a href="https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1">https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1</a>山景房里的三只猫</p></video>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[干净的 TypeScript 项目在编译时报错 Cannot find module 'undici-types' 的原因和解决]]></title>
            <link>https://chengpeiquan.com/article/typescript-error-cannot-find-module-undici-types</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/typescript-error-cannot-find-module-undici-types</guid>
            <pubDate>Sat, 06 Apr 2024 13:55:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>很久前配合 <a href="https://vue3.chengpeiquan.com/">《前端工程化：基于 Vue.js 3.0 的设计与实践》</a> 一书在 TypeScript 章节里讲解的内容，提供了一个很干净的 demo （见 <a href="https://github.com/learning-vue3/hello-node">hello-node</a> ），这里的 “干净” 是指除了必要的基础技术栈外，没有过多的第三方依赖，一直运行良好。</p>
<p>当然在教程里还是主动引导读者自己从零开始创建这个 Hello 项目，这也带来了这个假期遇到的一个读者反馈的问题。</p>
<h2 id="事情起因"><a tabindex="-1" href="#事情起因"><span></span></a>事情起因</h2>
<p>前几天在 GitHub Issue 的评论区，有位读者和我反馈说在运行 <code>npm run build</code> 时出现类似下方的报错，无法正确编译（见 <a href="https://github.com/chengpeiquan/learning-vue3/issues/193#issuecomment-2036541817">#193 (comment)</a> ）。</p>
<img src="https://cdn.chengpeiquan.com/img/2024/04/202404062221402.jpg?x-oss-process=image/interlace,1" alt="读者反馈">
<h2 id="触发错误日志"><a tabindex="-1" href="#触发错误日志"><span></span></a>触发错误日志</h2>
<p>遇到反馈的问题，首先是要先复现问题，于是先把仓库里的演示项目拉下来跑了一下，依然可以正常运行，但因为 “自己的代码自己清楚” ，马上联想到一个区别，就是读者自己创建的项目，依赖可能都是最新版，而我的演示项目由于 package.json 和 package-lock.json 里的版本号已有指定，因此 node_modules 下安装好的依赖可能并不完全一样，所以在演示仓库的项目里，这个错误没有被触发。</p>
<p>因此我把 node_modules 和 package-lock.json 文件删除，再重新安装依赖，确实，现在演示项目也无法通过编译了，还好日志很清晰，报错是来自 <code>node_modules/@types/node/globals.d.ts</code> 这个文件：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">➜</span><span style="color:#50A14F;--shiki-dark:#CE9178">  hello-node</span><span style="color:#50A14F;--shiki-dark:#CE9178"> git:</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">main</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#50A14F;--shiki-dark:#CE9178">✗</span><span style="color:#50A14F;--shiki-dark:#CE9178"> npm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> run</span><span style="color:#50A14F;--shiki-dark:#CE9178"> build</span></span>
<span class="line"></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">> @learning-vue3/node@1.0.0 build</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">> tsc src/ts/index.ts --outDir dist --target es6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">node_modules/@types/node/globals.d.ts:6:76</span><span style="color:#50A14F;--shiki-dark:#CE9178"> -</span><span style="color:#50A14F;--shiki-dark:#CE9178"> error</span><span style="color:#50A14F;--shiki-dark:#CE9178"> TS2792:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Cannot</span><span style="color:#50A14F;--shiki-dark:#CE9178"> find</span><span style="color:#50A14F;--shiki-dark:#CE9178"> module</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'undici-types'.</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Did</span><span style="color:#50A14F;--shiki-dark:#CE9178"> you</span><span style="color:#50A14F;--shiki-dark:#CE9178"> mean</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> set</span><span style="color:#50A14F;--shiki-dark:#CE9178"> the</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'moduleResolution'</span><span style="color:#50A14F;--shiki-dark:#CE9178"> option</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'node',</span><span style="color:#50A14F;--shiki-dark:#CE9178"> or</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> add</span><span style="color:#50A14F;--shiki-dark:#CE9178"> aliases</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> the</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'paths'</span><span style="color:#50A14F;--shiki-dark:#CE9178"> option?</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">6</span><span style="color:#50A14F;--shiki-dark:#CE9178"> type</span><span style="color:#50A14F;--shiki-dark:#CE9178"> _Request</span><span style="color:#50A14F;--shiki-dark:#CE9178"> =</span><span style="color:#50A14F;--shiki-dark:#CE9178"> typeof</span><span style="color:#50A14F;--shiki-dark:#CE9178"> globalThis</span><span style="color:#50A14F;--shiki-dark:#CE9178"> extends</span><span style="color:#50A14F;--shiki-dark:#CE9178"> {</span><span style="color:#50A14F;--shiki-dark:#CE9178"> onmessage:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> any</span><span style="color:#50A14F;--shiki-dark:#CE9178"> }</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ?</span><span style="color:#50A14F;--shiki-dark:#CE9178"> {}</span><span style="color:#50A14F;--shiki-dark:#CE9178"> :</span><span style="color:#50A14F;--shiki-dark:#CE9178"> import</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">"undici-types"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span><span style="color:#50A14F;--shiki-dark:#CE9178">.Request</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">                                                                             ~~~~~~~~~~~~~~</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">node_modules/@types/node/globals.d.ts:7:77</span><span style="color:#50A14F;--shiki-dark:#CE9178"> -</span><span style="color:#50A14F;--shiki-dark:#CE9178"> error</span><span style="color:#50A14F;--shiki-dark:#CE9178"> TS2792:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Cannot</span><span style="color:#50A14F;--shiki-dark:#CE9178"> find</span><span style="color:#50A14F;--shiki-dark:#CE9178"> module</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'undici-types'.</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Did</span><span style="color:#50A14F;--shiki-dark:#CE9178"> you</span><span style="color:#50A14F;--shiki-dark:#CE9178"> mean</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> set</span><span style="color:#50A14F;--shiki-dark:#CE9178"> the</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'moduleResolution'</span><span style="color:#50A14F;--shiki-dark:#CE9178"> option</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'node',</span><span style="color:#50A14F;--shiki-dark:#CE9178"> or</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> add</span><span style="color:#50A14F;--shiki-dark:#CE9178"> aliases</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> the</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'paths'</span><span style="color:#50A14F;--shiki-dark:#CE9178"> option?</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">7</span><span style="color:#50A14F;--shiki-dark:#CE9178"> type</span><span style="color:#50A14F;--shiki-dark:#CE9178"> _Response</span><span style="color:#50A14F;--shiki-dark:#CE9178"> =</span><span style="color:#50A14F;--shiki-dark:#CE9178"> typeof</span><span style="color:#50A14F;--shiki-dark:#CE9178"> globalThis</span><span style="color:#50A14F;--shiki-dark:#CE9178"> extends</span><span style="color:#50A14F;--shiki-dark:#CE9178"> {</span><span style="color:#50A14F;--shiki-dark:#CE9178"> onmessage:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> any</span><span style="color:#50A14F;--shiki-dark:#CE9178"> }</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ?</span><span style="color:#50A14F;--shiki-dark:#CE9178"> {}</span><span style="color:#50A14F;--shiki-dark:#CE9178"> :</span><span style="color:#50A14F;--shiki-dark:#CE9178"> import</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">"undici-types"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span><span style="color:#50A14F;--shiki-dark:#CE9178">.Response</span><span style="color:#383A42;--shiki-dark:#D4D4D4">;</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">                                                                              ~~~~~~~~~~~~~~</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">//</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ...</span></span></code></pre>
<h2 id="分析错误日志"><a tabindex="-1" href="#分析错误日志"><span></span></a>分析错误日志</h2>
<p>由于这个项目是很入门的演示项目，主要为了演示 Common JS 模块和 ES Module 模块的开发，以及一些 TypeScript 语法的入门，并没有涉及到 Node.js API 的操作，因此也没有主动去安装 <code>@types/node</code> 这个包。</p>
<blockquote>
<p>这里顺便补充个说明： <code>@types/node</code> 包主要是为 TypeScript 提供 Node.js API 的类型定义，如果在项目里调用了 Node.js 的 API ，则需要显式安装它，使 TypeScript 可以识别到这些 API 。</p>
</blockquote>
<p>所以 <code>@types/node</code> 这个包只能是第三方依赖带进来一并被安装的，为了方便排查，重新克隆了一个演示项目的原版，并通过 <code>npm list @types/node</code> 查看可以正常 <code>build</code> 时的依赖版本号，以及是哪个包引入的这个依赖。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">➜</span><span style="color:#50A14F;--shiki-dark:#CE9178">  hello-node-original</span><span style="color:#50A14F;--shiki-dark:#CE9178"> git:</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">main</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#50A14F;--shiki-dark:#CE9178">✗</span><span style="color:#50A14F;--shiki-dark:#CE9178"> npm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> list</span><span style="color:#50A14F;--shiki-dark:#CE9178"> @types/node</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">@learning-vue3/node@1.0.0</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /Users/chengpeiquan/Documents/projects/demo/h2</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">└─┬</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ts-node@10.9.1</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">  └──</span><span style="color:#50A14F;--shiki-dark:#CE9178"> @types/node@18.11.0</span></span></code></pre>
<p>此时正常 <code>build</code> 的 <code>@types/node</code> 版本号是 <code>18.11.0</code> ，是从 <code>ts-node</code> 引入的。</p>
<p>同样的命令在有问题的项目下运行，得到不同的版本号 <code>20.12.5</code> 。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">➜</span><span style="color:#50A14F;--shiki-dark:#CE9178">  hello-node</span><span style="color:#50A14F;--shiki-dark:#CE9178"> git:</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">main</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) </span><span style="color:#50A14F;--shiki-dark:#CE9178">✗</span><span style="color:#50A14F;--shiki-dark:#CE9178"> npm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> list</span><span style="color:#50A14F;--shiki-dark:#CE9178"> @types/node</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">@learning-vue3/node@1.0.0</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /Users/chengpeiquan/Documents/projects/demo/hello-node</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">└─┬</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ts-node@10.9.2</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">  └──</span><span style="color:#50A14F;--shiki-dark:#CE9178"> @types/node@20.12.5</span></span></code></pre>
<p>查看项目 <code>node_modules/ts-node</code> 目录下的 package.json 文件，看到 <code>ts-node</code> 对 <code>@types/node</code> 的依赖版本号是设置为 <code>*</code> 号，也就是通配符（下面是关键信息的列举，非全部）。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "name"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"ts-node"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "version"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"10.9.2"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "peerDependencies"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "@types/node"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"*"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>通配符版本号是指允许任何版本的依赖项，会安装最新可用版本，这也是为什么删除了 node_modules 目录和 package-lock.json 文件后，重新安装依赖后版本变化这么大的原因。</p>
<p>关于这个 <code>undici-types</code> 依赖，查看了 <code>@types/node</code> 的 package.json 文件，确实在后面的版本里引入其作为 <code>dependencies</code> 依赖，而之前的版本并没有，在 GitHub 溜达了一圈，原因可能来自 <a href="https://github.com/nodejs/undici/issues/2261">Node.js Undici 的这个 issue</a> ）。</p>
<h2 id="解决问题"><a tabindex="-1" href="#解决问题"><span></span></a>解决问题</h2>
<p>原因查明，解决方案就好办了，这里提供两个有效的解决方案。</p>
<h3 id="使用-skiplibcheck-选项"><a tabindex="-1" href="#使用-skiplibcheck-选项"><span></span></a>使用 skipLibCheck 选项</h3>
<p>由于 demo 的报错主要来自第三方库的代码检查（ TypeScript 默认会检查所有代码），在实际的项目开发中为了节省编译时间和跳过源码之外的问题报错，通常会启用 <code>skipLibCheck</code> 选项通知 TypeScript 跳过这些依赖库的类型检查（扩展名为 <code>.d.ts</code> 的文件），从而只检查开发者编写的源代码。</p>
<p>这也是为什么写了那么久的 TypeScript 从来没有遇到这种问题的原因，因为在实际项目里一直都是跳过对第三方库的检查啊哈哈哈。</p>
<p>选择这个方案的话，如果是走 CLI 选项编译，可以在命令里添加一个 <code>--skipLibCheck</code> 选项：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// package.json</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "scripts"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "build"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"tsc src/ts/index.ts --outDir dist --target es6 --skipLibCheck"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>如果是通过 tsconfig.json 配置编译选项，则是添加在 <code>compilerOptions</code> 里：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// tsconfig.json</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "compilerOptions"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "target"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"es6"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "module"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"es6"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "outDir"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"./dist"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "skipLibCheck"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#0184BC;--shiki-dark:#569CD6">true</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>关于 <a href="https://www.typescriptlang.org/tsconfig#skipLibCheck">skipLibCheck 选项</a> 的更多说明可以在 TypeScript 官网文档上查阅。</p>
<h3 id="添加-moduleresolution"><a tabindex="-1" href="#添加-moduleresolution"><span></span></a>添加 moduleResolution</h3>
<p>除了 <code>skipLibCheck</code> ，还有一个解决方案，还记得错误日志吗？在错误日志里给出了两个解决方案的建议：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Did</span><span style="color:#50A14F;--shiki-dark:#CE9178"> you</span><span style="color:#50A14F;--shiki-dark:#CE9178"> mean</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> set</span><span style="color:#50A14F;--shiki-dark:#CE9178"> the</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'moduleResolution'</span><span style="color:#50A14F;--shiki-dark:#CE9178"> option</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'node',</span><span style="color:#50A14F;--shiki-dark:#CE9178"> or</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> add</span><span style="color:#50A14F;--shiki-dark:#CE9178"> aliases</span><span style="color:#50A14F;--shiki-dark:#CE9178"> to</span><span style="color:#50A14F;--shiki-dark:#CE9178"> the</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'paths'</span><span style="color:#50A14F;--shiki-dark:#CE9178"> option?</span></span></code></pre>
<p>由于这是一个第三方库的报错，因此 <code>paths</code> 方案不适用（该方案适合对源码目录下的文件配置 Alias 别名）。</p>
<p>因此可以通过另外一个建议，添加 <code>--moduleResolution</code> 选项。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// package.json</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "scripts"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "build"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"tsc src/ts/index.ts --outDir dist --target es6 --moduleResolution node"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>也可以成功解决编译问题，同理，也可以在 tsconfig.json 里配置该选项：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-json"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">// tsconfig.json</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">{</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">  "compilerOptions"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "target"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"es6"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "module"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"es6"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "outDir"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"./dist"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    "moduleResolution"</span><span style="color:#383A42;--shiki-dark:#D4D4D4">: </span><span style="color:#50A14F;--shiki-dark:#CE9178">"Node"</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  }</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">}</span></span></code></pre>
<p>关于 <a href="https://www.typescriptlang.org/tsconfig#moduleResolution">moduleResolution 选项</a> 的更多说明可以在 TypeScript 官网文档上查阅。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[macOS 基于 Android Studio 修改模拟器 Hosts]]></title>
            <link>https://chengpeiquan.com/article/macos-android-studio-emulator-hosts</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/macos-android-studio-emulator-hosts</guid>
            <pubDate>Sat, 10 Feb 2024 13:03:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>用回 macOS 一段时间了，各种软件对 M 系列芯片基本上也都有支持的版本，除了安卓模拟器 - - 。之前在 Windows 上面常用的 MuMu 、雷电、夜神等模拟器，不是没有 Mac 版，就是虽然有 Mac 版，但不支持 M 系列芯片（ ARM 架构），所以抛弃一系列国产模拟器，回归 Android Studio 。</p>
<h2 id="创建模拟器"><a tabindex="-1" href="#创建模拟器"><span></span></a>创建模拟器</h2>
<p>安装和创建模拟器的过程不赘述了，切记要修改 Hosts 的话，需要模拟器支持 Root ，因此不能选择 Google Play 的 System Image ，否则只能以 Readonly 模式启动模拟器，无法 Root 。</p>
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102127890.jpg?x-oss-process=image/interlace,1" alt="不要选择 Google Play">
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102127906.jpg?x-oss-process=image/interlace,1" alt="选择这种普通的">
<h2 id="启动可写权限的模拟器"><a tabindex="-1" href="#启动可写权限的模拟器"><span></span></a>启动可写权限的模拟器</h2>
<p>需要通过命令行以 Writable 的模式启动，这个时候不需要开启 Android Studio 了（从 AS 启动的模拟器只有 Readonly 权限）。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 访问 Android Studio 的模拟器文件夹</span></span>
<span class="line"><span style="color:#0184BC;--shiki-dark:#DCDCAA">cd</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ~/Library/Android/sdk/emulator/</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 查询所有的模拟器列表</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">./emulator</span><span style="color:#986801;--shiki-dark:#569CD6"> -list-avds</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 添加可写权限进行启动（这里的 Pixel_7_Pro_API_33 是模拟器名称）</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">./emulator</span><span style="color:#986801;--shiki-dark:#569CD6"> -avd</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Pixel_7_Pro_API_33</span><span style="color:#986801;--shiki-dark:#569CD6"> -writable-system</span></span></code></pre>
<p>此时模拟器就运行起来，命令行不要关闭，否则模拟器也会退出。</p>
<h2 id="检查-adb-启用状态"><a tabindex="-1" href="#检查-adb-启用状态"><span></span></a>检查 ADB 启用状态</h2>
<p>下一步操作需要通过 <code>adb</code> 命令来执行（关于 ADB 是什么，详细可以见 Android 的官方文档：<a href="https://developer.android.com/tools/adb?hl=zh-cn">Android 调试桥 (adb)</a> ），但这里只是想确认它是否存在。</p>
<p>所以直接看 StackOverflow 的操作即可：<a href="https://stackoverflow.com/questions/17901692/set-up-adb-on-mac-os-x">Set up adb on Mac OS X</a> 。 因为安装 Android Studio 的时候就默认一起装了，所以直接看里面的 “Option 3 - If you already have Android Studio installed” 部分。</p>
<p>注意：我是使用 <a href="https://github.com/ohmyzsh/ohmyzsh">Oh My Zsh</a> 的 ZSH Shell ，所以配置文件和 StackOverflow 答案里不一样，记得留意自己用的是哪个 Shell 。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#0184BC;--shiki-dark:#DCDCAA">echo</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'export ANDROID_HOME=/Users/$USER/Library/Android/sdk'</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> >> </span><span style="color:#50A14F;--shiki-dark:#CE9178">~/.zshrc</span></span>
<span class="line"><span style="color:#0184BC;--shiki-dark:#DCDCAA">echo</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 'export PATH=${PATH}:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools'</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> >> </span><span style="color:#50A14F;--shiki-dark:#CE9178">~/.zshrc</span></span>
<span class="line"><span style="color:#0184BC;--shiki-dark:#DCDCAA">source</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ~/.zshrc</span></span></code></pre>
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102146502.png?x-oss-process=image/interlace,1" alt="我的 .zshrc 配置">
<p>设置后重启 Terminal ，通过 <code>adb version</code> 命令查看是否启用成功。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">adb</span><span style="color:#50A14F;--shiki-dark:#CE9178"> version</span></span>
<span class="line"></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Android</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Debug</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Bridge</span><span style="color:#50A14F;--shiki-dark:#CE9178"> version</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 1.0.41</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Version</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 34.0.5-10900879</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Installed</span><span style="color:#50A14F;--shiki-dark:#CE9178"> as</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /Users/chengpeiquan/Library/Android/sdk/platform-tools/adb</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Running</span><span style="color:#50A14F;--shiki-dark:#CE9178"> on</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Darwin</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 23.1.0</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (arm64)</span></span></code></pre>
<h2 id="修改-hosts"><a tabindex="-1" href="#修改-hosts"><span></span></a>修改 Hosts</h2>
<p>首先确认模拟器编号，通过命令行启动模拟器后，在设置界面可以看到这个 <code>5554</code> 端口号（可能是别的端口）。</p>
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102220605.jpg?x-oss-process=image/interlace,1" alt="通过命令行启动模拟器">
<p>或者是通过 Android Studio 启动模拟器，也可以看到端口号。</p>
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102220607.png?x-oss-process=image/interlace,1" alt="通过 Android Studio 启动模拟器">
<p>开始修改 Hosts 啦。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 通过 ADB 开启 Root 权限</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">adb</span><span style="color:#50A14F;--shiki-dark:#CE9178"> root</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 挂载已启动的模拟器文件，这里记得对应模拟器的端口号</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">adb</span><span style="color:#986801;--shiki-dark:#569CD6"> -s</span><span style="color:#50A14F;--shiki-dark:#CE9178"> emulator-5554</span><span style="color:#50A14F;--shiki-dark:#CE9178"> remount</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 拉取模拟器的文件到桌面，方便修改</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">adb</span><span style="color:#50A14F;--shiki-dark:#CE9178"> pull</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /system/etc/hosts</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ~/Desktop/hosts</span></span></code></pre>
<p>接下来就和之前在 Windows 上修改 Hosts 一样啦，改完后把文件 push 回去。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 改完后把 hosts 文件 push 回去</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">adb</span><span style="color:#50A14F;--shiki-dark:#CE9178"> push</span><span style="color:#50A14F;--shiki-dark:#CE9178"> ~/Desktop/hosts</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /system/etc/hosts</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 查看模拟器上的 hosts 文件内容，确认是否修改成功</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">adb</span><span style="color:#50A14F;--shiki-dark:#CE9178"> shell</span><span style="color:#50A14F;--shiki-dark:#CE9178"> cat</span><span style="color:#50A14F;--shiki-dark:#CE9178"> /etc/hosts</span></span></code></pre>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[给猫儿子的新年礼物 它很喜欢！]]></title>
            <link>https://chengpeiquan.com/article/new-mac-2024</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/new-mac-2024</guid>
            <pubDate>Sun, 14 Jan 2024 09:33:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>哈哈哈哈哈上一次买 Mac 还是 2015 年，然后在 2019 年换了 Windows 台式机用到现在。</p>
<p>最近跑了 5 年的台式机又感觉配置不太够用了，干脆再换回 Mac ，开心！</p>
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102244502.jpg?x-oss-process=image/interlace,1" alt="">
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102244503.jpg?x-oss-process=image/interlace,1" alt="">
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102244504.jpg?x-oss-process=image/interlace,1" alt="">
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102244506.jpg?x-oss-process=image/interlace,1" alt="">
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102244507.jpg?x-oss-process=image/interlace,1" alt="">
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102244508.jpg?x-oss-process=image/interlace,1" alt="">
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102244509.jpg?x-oss-process=image/interlace,1" alt="">
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102244510.jpg?x-oss-process=image/interlace,1" alt="">
<img src="https://cdn.chengpeiquan.com/img/2024/02/202402102244511.jpg?x-oss-process=image/interlace,1" alt="">]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[年终总结：2023 年的一些回顾和 2024 年的一些小规划]]></title>
            <link>https://chengpeiquan.com/article/2023-year-end-summary</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/2023-year-end-summary</guid>
            <pubDate>Sat, 30 Dec 2023 06:52:00 GMT</pubDate>
            <content:encoded><![CDATA[<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230212147.jpg?x-oss-process=image/interlace,1" alt="我家三只超级乖的猫">
<p>在落笔之前想起一个古早笑话……</p>
<blockquote>
<p>快六点了，街边卖油条的还没来，我只能拨通他的电话，大哥在那边说：我卖了这么多年的油条了，从来自由自在的，自从遇见你之后，我居然有了上班的感觉。</p>
</blockquote>
<p>因为我被催更了哈哈哈哈哈（见 <a href="https://github.com/chengpeiquan/chengpeiquan.com/issues/363">#363</a> ）。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230161603.jpg?x-oss-process=image/interlace,1" alt="催更现场">
<p>本来今年的总结第一句话想写 “2023 对我算是比较特殊的一年” ，结果看了一眼去年的回顾，去年也是这么写的…… 那今年再强化一下，改成 “2023 对我是非常特殊的一年” 哈哈哈哈哈哈哈，确实非常特殊的一年，因为经历了很多之前没有经历过的事情。</p>
<h2 id="人生中的第一次失业"><a tabindex="-1" href="#人生中的第一次失业"><span></span></a>人生中的第一次失业</h2>
<p>去年写总结的时候还是元旦放假前，当时的我对于三天后会发生什么事还一无所知，但也不能说完全一无所知吧，作为一名程序员和刑侦爱好者，上家公司总是结合疫情的风吹草动安排半薪和基薪休假（广州当时其实就海珠区比较严重，其他区域已经放开，我们都不在海珠区，这么喜欢跟风就很不对劲……），所以就已经感觉到前公司当时的运作要崩溃了，只是没想到来的这么快而已。那时候元旦假期刚过完，回去上班第一天前老板就开会说融资失败要执笠了，团队要解散。于是我刚休完元旦假期，上了一天班又继续放假了，当然这次是无薪假期哈哈哈哈。</p>
<p>这是我人生中第一次在没下家的情况下直接失业，既然前面有所感知，为什么没有骑驴找马呢？因为团队里都是我合作了很多年的 Partners ，都是我在网易从 2016 年起陆续认识，一起玩了六七年的 Friends 和 Leader ，包括从网易当时的总监熬不住先跳槽了，我们一起被合并到边缘部门，再一起离职去深圳待了两年，再一起回广州，经历了很多事情，所以表面上连网易在内到当时失业，经历的是三家公司，但实际上一直都在一个团队里（包括前老板以前也是网易的总监，所以那段时间首先想的是有难同当，低谷期总是有的，熬过去就好了）。</p>
<p>过年前失业确实是一个很尴尬的节点，大部分公司在年底是没有招聘计划的，通常会在二月底到三月，等新一年的目标定好才会开始陆续招人，所以那个时候想走内推也没有什么公司有 HC ，也尝试谈了几家创业公司，也没有那么大的团队缺口，都是最多只要一个经验丰富点的来带头开荒就顶天了，盘不下几个人的团队一起去，加上做的事情比较无聊和薪资没谈拢，干脆就等过完年再看机会。</p>
<h2 id="失业期间在干什么"><a tabindex="-1" href="#失业期间在干什么"><span></span></a>失业期间在干什么</h2>
<p>虽然人到中年还是 “三无” 选手（无老婆、无小孩、无房贷压力），但这个时候 “三无” 属性反而使我的抗风险能力达到了 Max ，没有银行给我的还款压力，没有人在旁边唠叨给我精神压力，身边还有三只懂事粘人的猫，所以那段时间心态也比较平稳，工作方面没了事情做就没事情做吧，我自己还有很多事情要搞（在 GitHub 上列了一堆 Projects ），还是忙个不停。</p>
<h3 id="改稿子"><a tabindex="-1" href="#改稿子"><span></span></a>改稿子</h3>
<p>那个时候已经和出版社签了合同，准备出版我的书，基于出版规范很多细节要调整，包括不允许出现 “你我他” 这些人称代词，还有很多描述不能口语化，更重要的是保证代码的准确性，平时苦于每天在睡前挤那一两个小时在开夜车改稿，一下子突然有了很多时间让我慢慢改，反而有一种莫名其妙的幸福感…… 每天终于不用那么累了，于是对着 83 页 PDF 的规范手册，几十万字的稿子硬是改完了。</p>
<p>这是后来寄过来校对第二次的清样，哈哈哈哈也别被吓到，单面印刷的所以看起来厚到吓人。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230170944.jpg?x-oss-process=image/interlace,1" alt="薄如蝉翼的稿袋">
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230170945.jpg?x-oss-process=image/interlace,1" alt="也就几页稿子…">
<p>后来这本书在五一期间正式上市了，围绕前端工程化的入门，虽说前端娱乐圈，框架一个又一个，但真正被企业认可的以及基础方面的知识主要还是没太大变化，书的内容的有效性应该可以维持上几年，刚入门前端觉得有需要的同学可以支持一下，感谢您！</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230172538.jpg?x-oss-process=image/interlace,1" alt="《前端工程化：基于 Vue.js 3.0 的设计与实践》">
<p>相关阅读：<a href="https://chengpeiquan.com/article/the-story-of-the-book-learning-vue3.html">我写了一本书《前端工程化：基于 Vue.js 3.0 的设计与实践》 想分享一下它背后的故事</a></p>
<p>当时给身边好友写的签名本，好羞涩哈哈哈哈哈。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230172539.jpg?x-oss-process=image/interlace,1" alt="人生第一次在自己的书上签名">
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230172540.jpg?x-oss-process=image/interlace,1" alt="真的很羞涩哈哈哈哈">
<h3 id="做设计"><a tabindex="-1" href="#做设计"><span></span></a>做设计</h3>
<p>去年的总结说要对 <a href="https://preset.js.org">create-preset</a> 改版，程序至今还是没时间改，倒是对官网先重新设计了一版，还自己鼠绘了个 LOGO ，一年了，自己看起来还是很满意哈哈哈哈哈哈。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230172804.jpg?x-oss-process=image/interlace,1" alt="当时的票圈">
<p>还想设计设计自己的新文身，从去年回广州就想加图案，但因为忙也没时间搞，有时间的时候也会胡乱画画找找灵感（红色的是画上去的，最终还是没确定下来，后面也没有时间了就又搁置了，新的一年再看看吧）。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230173612.jpg?x-oss-process=image/interlace,1" alt="乱画一通">
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230185256.jpg?x-oss-process=image/interlace,1" alt="第一个文身的由来">
<h3 id="戒掉了网络游戏"><a tabindex="-1" href="#戒掉了网络游戏"><span></span></a>戒掉了网络游戏</h3>
<p>当年作为一个资深玩家进的游戏行业，又凭着丰富的游戏经验跳去了网易游戏，没想到有一天我也会不玩网络游戏了，一月份我弃坑了玩了五年的某手游（排名在全区第二），一方面是失业带来的危机感让我觉得继续玩这种时不时要充值的游戏没什么意义，另外一方面是狗策划安排的活动时间实在是过于挤压我为数不多的休息时间，既然花了钱去玩还得不到快乐，那就干脆就不玩了。</p>
<p>在成为 “不抽烟不喝酒不玩游戏” 的好男人一段时间后，有时候很无聊又想玩玩游戏消磨下疲劳，所以现在又像小孩子一样，重新玩起了小时候玩的那些游戏，在模拟器玩 FC 和街机，还有老头滚动条之类的经典单机。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230174154.jpg?x-oss-process=image/interlace,1" alt="红白机永远的神！">
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230190303.jpg?x-oss-process=image/interlace,1" alt="双截龙 Ⅱ">
<h3 id="其他的事情"><a tabindex="-1" href="#其他的事情"><span></span></a>其他的事情</h3>
<p>其他事情主要就是改简历和准备面试的事情，失业时间说短不短，说长也不长，刚好过年前到元宵后休息了两个月，二月底内推到现在的这家公司面试通过后就入职了。</p>
<h2 id="新工作是干什么的"><a tabindex="-1" href="#新工作是干什么的"><span></span></a>新工作是干什么的</h2>
<p>现在在一家做 NAS 方向的初创公司做开发，依然是自研团队。目前项目里 C++ 负责 NAS 操作系统的开发，而前端这边是负责把操作系统上的功能实现为界面化的交互（像 Windows / macOS 那样），所以这是一个很重前端的项目，比起传统的 C 端应用或者 B 端一些低代码平台，这个项目我觉得更有意思，也很考验开发者的 TypeScript 功底，目前来了半年多，感觉自己的能力和理解又有了很多提升，当然也离不开身边大佬们的指导和帮助。</p>
<blockquote>
<p>产品还在开发中，等上线了我再公开哈哈哈哈</p>
</blockquote>
<p>另外还有一个很神奇的缘分，就是现在的老板其实也是我十年前的老板， PP 助手的创始人（早期越狱那个年代的 iOS 用户应该都知道？），我之前也是在 PP 助手团队，后来跟着被收购到了 UC ，再被收购到阿里，被阿里收购后因为阿里部门间合并后的一些后遗症，做的不开心了，我才跳去了网易。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231231131332.jpg?x-oss-process=image/interlace,1" alt="UC 十周年时，当年发的朋友圈">
<p>兜兜转转又回来了以前的团队，说不出的兴奋感，因为这里的人也很好，又是一见如故，哪怕多年未见，连我的花名都还记得！虽然离开了一群合作很久的 Partners ，但又回到了另一个很好的 Team ，很开心的一年！现在这里除了以前 PP 的老友，也有后面在 UC 和阿里的同事，虽然有些之前并未谋面，但很快就达成了默契，很合拍的工作节奏！</p>
<h2 id="开源社区"><a tabindex="-1" href="#开源社区"><span></span></a>开源社区</h2>
<p>今年还是活跃在 GitHub 上（截图生成自 <a href="https://github-contributions.vercel.app">GitHub Contributions</a> ），可能去年码字太多，今年几乎没写什么文章，一直在敲代码，弥补之前没做的很多事情。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230203922.jpg?x-oss-process=image/interlace,1" alt="2023 年在 GitHub 的活跃情况">
<p>今年敲代码的时间比去年高了不少，晒一下这一年最高敲代码时间的一周，沉浸式敲代码哇哈哈…… （统计数据来自 <a href="https://wakatime.com">WakaTime</a> ），我记得有一天敲了 12h 的代码，太早的统计要付费才能看，忘记留个图纪念下。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230202751.jpg?x-oss-process=image/interlace,1" alt="2023-12-03">
<p>这么长的敲代码时间倒不是因为公司加班，公司除了版本 DDL 前后会加加班之外，都是六点半下班和双休。</p>
<p>时间方面主要是在这几个地方：</p>
<ol>
<li>完善自己的工具链（ e.g. 贝斯手 <a href="https://github.com/chengpeiquan/bassist">Bassist</a> 系列，还有一些没公开的），把自己常用的工具和配置都集合到一个 Monorepo 管理，避免每次用的时候都要加一堆配置或者重新实现</li>
<li>踩 React 的坑……（下面说）</li>
<li>踩其他技术栈的坑（例如尝试写了一点 Rust …… 还有各种 demo 吧，不是开源，但也都托管在 GitHub 上，所以活跃度就这么来的…… ）</li>
</ol>
<p>关于踩 React 的坑，去年在总结的新年展望里提到要好好写一下 React ，倒不是说 Vue 不好，相反，真的把主技术栈切到 React 后才发现 Vue 帮开发者做了好多优化，而 React 好多要靠开发者自己处理…… Vue 真是保姆级！</p>
<p>之所以在出了一本 Vue 的书之后，自己写起了 React ，是因为我写多了想换换口味，之前因为公司一直都是 Vue 为主，所以不知不觉也写了得有五年多的 Vue 了，不想像以前读书一样，只偏科数学和物理，文科一塌糊涂，最后总成绩就显得很一般。目前在 Vue 自己也算是有一套自己的最佳实践，平时遇到问题也可以很快排查，而 React 还没有达到这种状态，所以今年主要也在解决自己的偏科问题。</p>
<p>刚好今年换工作后是新团队新项目，涉及到技术选型，新团队的 Partners 都很一致的选择用 React 来写，所以我也很符合个人预期的都在写 React 了。</p>
<h2 id="其他的感慨"><a tabindex="-1" href="#其他的感慨"><span></span></a>其他的感慨</h2>
<p>首先， AI 真好用哈哈哈，自从 ChatGPT 出来后，工作再也离不开它了，目前主要用它消耗掉很多基础的工作，比如输出单元测试用例、 JSON 转 TypeScript 类型等等，还有遇到问题现在可能不是首选 Google 搜索了，而是在 GPT 先问一下，如果不靠谱再去 Google / StackOverflow / GitHub Issue 找答案。</p>
<p>再一个就是离开网易后很感慨一件事，就是刚转行的时候曾经的产品运营身份让我在技术路上走的蛮辛苦的，有人不认可，有人不信任，在当时确实是一个劣势，毕竟万事开头难，但经过这么多年的热爱和坚持，劣势反过来已经成为了自己的优势。</p>
<p>有一定的产品设计经验、有一定的用户体验优化能力、懂 SEO 懂运营懂用户，往产品工程师方向发展，比做一个纯技术的全栈工程师更现实一些（相对于我这种普通人的认知层面来说的，大佬肯定又有不一样的认知），当然现在托 Node.js 的福，只写 TypeScript 的我也可以写全栈了。</p>
<h2 id="新年愿望"><a tabindex="-1" href="#新年愿望"><span></span></a>新年愿望</h2>
<p>首先希望身体健康啊哈哈哈，距离上一次去医院不知道多少年了，今年才又进了一次医院，做了人生中的第一个小手术，当时耳朵因为细菌感染引起皮脂腺囊肿，去做了个引流手术，第一次动刀子，内心还是比较慌的，还好过程也没有啥感觉，打了个麻药，所以手术过程中还没有文身有感觉（我文身不打麻药！）</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230211312.jpg?x-oss-process=image/interlace,1" alt="惨兮兮的朋友圈">
<p>其他的还是先把原来还没搞完的事情搞起来吧，比如想重构博客一年多了，结果因为这个那个事情，一直拖着没处理，一下子距离上一次重构都已经三年了，真是见鬼啊哈哈哈哈时间怎么这么快！！！</p>
<p>都记录在 GitHub 的 Projects 里管理了，等做到了再说吧，不立那么多 Flag ，免得一年又一年说了又不做哈哈哈哈哈。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/12/20231230211903.jpg?x-oss-process=image/interlace,1" alt="好多计划都赶不上变化哈哈哈哈">
<p>谢谢阅读！</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[千元预算组装入门 NAS 设备 分享 NAS 的硬件基础知识]]></title>
            <link>https://chengpeiquan.com/article/my-custom-nas</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/my-custom-nas</guid>
            <pubDate>Sat, 10 Jun 2023 15:39:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>最近在一千元预算内搞了一台 NAS ，在这个过程也算蛮自定义的搭配，记录一下这个过程中的主要知识点和最终搭配结果。</p>
<p>先补一个 2025 的更新，回想当时把闲置的猫航空箱用来装 NAS 真是明智，在 24 年国庆的时候又搞了个五盘位的硬盘柜，还是很能装！目前的搭配是：小主机 + 外接硬盘柜 搭配 <a href="https://www.fnnas.com?utm_source=chengpeiquan.com">飞牛私有云 fnOS</a> NAS 系统，主要搞下影视和相册，个人体验非常好！</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501070003368.jpg?x-oss-process=image/interlace,1" alt="目前最新的搭配，小主机 + 外接硬盘柜">
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501070018282.jpg?x-oss-process=image/interlace,1" alt="飞牛 NAS 界面">
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501070018283.jpg?x-oss-process=image/interlace,1" alt="飞牛影视">
<p>目前自组 NAS 来说，免费并且简单上手、日程使用稳定等角度来说，飞牛的 NAS 系统是很不错的，可以在 <a href="https://www.fnnas.com/download?utm_source=chengpeiquan.com">飞牛官网下载</a> ，自己用 USB 烧个引导盘就能装，或者用 PVE 虚拟机跑也没问题。</p>
<h2 id="最终组装方案"><a tabindex="-1" href="#最终组装方案"><span></span></a>最终组装方案</h2>
<p>好了，看完现在最新的配置，回到最早的第一版，这是我当时的千元配置的最终方案。</p>
<p>毕竟第一次玩 NAS ，并且要存储的数据也不是多到特别多，所以出于在预算范围内的一个控制，还是尽量控制在一千块钱不要超出太多，最终我自己的方案如下：</p>
<table>
<thead>
<tr>
<th align="center">硬件</th>
<th align="left">链接</th>
<th align="center">价格</th>
<th align="left">尺寸</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">迷你主机</td>
<td align="left"><a href="https://item.jd.com/10039690351520.html">倍控 J4125 四核迷你微型工控机</a></td>
<td align="center">￥ 489.99</td>
<td align="left">136 x 127 x 40</td>
</tr>
<tr>
<td align="center">硬盘座</td>
<td align="left"><a href="https://item.jd.com/100014988528.html">绿联 3.5 英寸 USB 3.0 硬盘盒底座</a></td>
<td align="center">￥ 159</td>
<td align="left">162 x 112 x 62</td>
</tr>
<tr>
<td align="center">固态硬盘</td>
<td align="left"><a href="https://item.jd.com/3258936.html">金百达 240G mSATA 固态硬盘</a></td>
<td align="center">￥ 61.81</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">机械硬盘</td>
<td align="left"><a href="https://item.jd.com/10040397930046.html">西数机械硬盘 3.5 英寸 / 2T / CRM / 5400 转 / 64M 缓存</a></td>
<td align="center">￥ 189 x 2</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">内存条</td>
<td align="left"><a href="https://item.taobao.com/item.htm?id=558603479364">昱炎笔记本全兼容 DDR4 8G 2666MHz</a> （记得备注发三星颗粒）</td>
<td align="center">￥ 73</td>
<td align="left">-</td>
</tr>
</tbody>
</table>
<p>总价 ￥ 1161.8 ，在 618 前买的，部分配件还进行了保价退了点钱，迷你主机不需要风扇降温，而且送了电源适配器，所以很多配件的钱都省了下来。里面之所以有多一个固态硬盘是因为迷你主机支持插一个 mSATA 的 SSD ，刚好现在 SSD 价格又不贵，内置一个 SSD 用来装系统，然后机械硬盘就可以完全当存储盘用了。</p>
<p>关于为什么选择硬盘座，是因为我在广州，这边几乎不会有地震，我家三只猫也不会上桌（装监控 4 年了，没有被抓到），加上我有一个闲置的航空箱，我都放到航空箱里面了，安全和稳定性还是很高的，另外一方面是因为我对网口需求不大，有一些迷你主机是四五六个网口，只有两个 USB ，我这个是两个网口，有六个 USB ，用 USB 3.0 来接机械硬盘也不会很慢。</p>
<p>伪装起来还是有模有样的：</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611215548.jpg?x-oss-process=image/interlace,1" alt="伪装起来还是有模有样的">
<p>弟弟好像发现了里面有什么奇怪的东西：</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611215549.jpg?x-oss-process=image/interlace,1" alt="弟弟好像发现了里面有什么奇怪的东西">
<p>弟弟一回头：“我的航空箱呢？”</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611215550.jpg?x-oss-process=image/interlace,1" alt="弟弟一回头：“我的航空箱呢？”">
<p>再往前看：“果然是它”</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611215551.jpg?x-oss-process=image/interlace,1" alt="再往前看：“果然是它”">
<p>有一说一，航空箱内部的散热还是很不错的，而且空间很大，周围又可以穿线，真的很舒服！</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611215552.jpg?x-oss-process=image/interlace,1" alt="航空箱内部的散热还是很不错的">
<h2 id="前置知识"><a tabindex="-1" href="#前置知识"><span></span></a>前置知识</h2>
<p>继续回来讲初入 NAS 时的一些知识点，刚接触 NAS 的时候最迷茫的应该就是它的概念和用途了，就算了解了它的作用，想采购一个合适的 NAS 设备又会涉及到很多硬件知识。既然选择自己配 NAS 做一个 DIY 玩家，肯定还是需要先熟悉一些前置的硬件知识，否则大可花几千块钱买成品 NAS 了，所以有一些前置知识点还是值得先简单了解一下。</p>
<h3 id="什么是-nas-"><a tabindex="-1" href="#什么是-nas-"><span></span></a>什么是 NAS ?</h3>
<p>NAS 全称是 Network Attached Storage ，网络附属存储，关于它的概念可以查看 <a href="https://en.wikipedia.org/wiki/Network-attached_storage">Wikipedia 词条</a> 。用通俗一点的话来理解，它最直接的用途是当一个自建的云盘服务器，可以存放你的照片、视频、电影资源、重要资料数据等等，而存储介质是属于你自己的，除了自己常用的主机可以访问 NAS 单点存储备份外，还可以作为局域网、广域网共享，家里的其他设备也可以访问到 NAS 上的文件，如果体验过用 AirDrop 从 iPhone 向 Mac 共享文件的便利性，那么通过 NAS 也可以解决 iPhone 和 Windows 共享文件比较麻烦的问题。</p>
<p>借助 NAS 系统，分布式工作环境可以轻松地从任何联网设备访问文件和文件夹，在局域网里可以访问设备的内网 IP 即可访问到 NAS 系统的桌面操作界面，如果有公网 IP 或者做了内网穿透，还可以绑定自己的域名，不在家的时候也可以通过域名访问到家里的 NAS 系统，做到随时随地访问。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230610151033.jpg?x-oss-process=image/interlace,1" alt="借助 NAS 系统，分布式工作环境可以轻松地从任何联网设备访问文件和文件夹">
<p>因为 NAS 设备充当的是一个服务器的作用，所以在装好系统之后就不需要鼠标键盘和显示器了（系统安装过程中还是需要的，临时拔一下自己其他主机的鼠键和显示器过来用就行）， NAS 系统启动后在局域网内有一个固定的 IP ，使用其他设备（例如台式机、笔记本）去访问这个局域网 IP 即可访问到 NAS 操作系统的桌面界面，这个界面都是以 Web 的形式实现的。</p>
<p>以下是我现在在用的 <a href="https://www.fnnas.com?utm_source=chengpeiquan.com">飞牛 fnOS</a> 系统界面。</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501062320568.jpg?x-oss-process=image/interlace,1" alt="我在 MacBook 访问局域网 IP 即可进入 NAS 操作系统界面">
<h3 id="为什么要用-nas-"><a tabindex="-1" href="#为什么要用-nas-"><span></span></a>为什么要用 NAS ？</h3>
<p>在上面提到了 NAS 可以简单的理解为一个自建的云盘，存储介质都属于自己，数据的管理也都是自己，这一点有什么好处呢？</p>
<p>如果你在网上冲浪比较久，应该早就知道很多平台哪怕被用户信任了十几年，还是会因为盈利问题或者不可抗拒的原因关闭下线（例如 <a href="https://www.zhihu.com/question/314817041">国内某巨头的相册</a> 、 <a href="https://www.zhihu.com/question/51801176">国内某巨头的云盘</a>），虽然关停前会有一段时间让用户可以把自己的东西下载回本地，但有句话说的好，你永远不知道明天和意外哪个先来，可能在关停前短短的保数据阶段，会因为一些意外的事情导致无法及时处理，比如考研备战期间忙的要死根本无暇顾及太多外部杂事，或者是身体原因住院之类的，我自己也在 2020 年也有过挺长时间经历了卧床不能走路的日子，那段时间除了腿脚不便之外，整个人的精力也是前所未有的低谷期，很多时间根本没办法去及时关注。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230610142844.png?x-oss-process=image/interlace,1" alt="我不能走路那段时间的日记">
<p>或者是因为某些原因导致资源再也不能显示，比如我最喜欢的 Beyond 乐队的《长城》、《岁月无声》，三十多年前的摇滚乐了，国内外几亿人都喜欢的歌曲，因为被某些人的过度解读导致永远 404 。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230610162359.jpg?x-oss-process=image/interlace,1" alt="BEYOND 长城">
<p>还有南京市民李先生的所有音乐，陪我走过很多次低谷的《你离开了南京，从此没有人和我说话》、《忽然》、《关于郑州的记忆》、《梵高先生》等等正常歌曲也都是不能光明正大的在各大音乐平台搜到（之前保存在云盘里的也被和谐了，还好电脑里有备份）。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230610144232.jpg?x-oss-process=image/interlace,1" alt="南京市民李先生的音乐文件">
<p>数据无价，很多资料本身十分正常，但就是由于某些原因导致无法信任某些平台，像我自己写的 <a href="/cookbook">菜谱</a> 虽然主要分享在小红书上，但我也并不信任小红书，不说它会不会在某一天关闭业务，哪怕只是简单到因为审核到不和谐的内容然后就把我号封了，我写的东西也就没了（已经收到过系统通知说出现不适合推送的内容，所以本条笔记不做曝光，但不会告诉我要怎么修改），所以我才自己同步了一份数据到我自己的 GitHub 和自己的数据库做多端备份。</p>
<p>当然 NAS 除了做基础的存储，很多服务还可以自己配置用起来，比如现在各个平台都要会员，优酷开一个、爱奇艺开一个，网易云音乐开一个， QQ 音乐开一个，每个平台都有自己专属版权的资源，开多个平台的会员，这里也是很大的成本支出。</p>
<p>所以需不需要 NAS ，最终还是看个人需求，对我自己来说，有一个用来备份重要数据的 NAS 还是很有用的。</p>
<hr>
<h3 id="主要配件"><a tabindex="-1" href="#主要配件"><span></span></a>主要配件</h3>
<p>组 NAS 有很多种方案，最简单的是购买 NAS 厂商提供的成品，国内的像群晖、威联通、绿联、铁威马、极空间等等，优点就是直接购买成品，买回来也是开箱即用，缺点就是溢价太高，相比自己组装，随便一个基础型号都几千块钱，对于不是重度 NAS 用户来说，实在太贵了。</p>
<p>所以我选择了自己组装硬件来做一个 NAS ，方案我是准备了三套，就是放在最后面介绍的 <a href="#%E6%90%AD%E9%85%8D%E6%96%B9%E6%A1%88">搭配方案</a> 。当然有旧电脑也是可以用来装 NAS 系统当 NAS 设备用的。</p>
<p>一般来说硬盘都是要单独选购，而其他的如 CPU 、主板、机箱等配件通常会放在一起选购，下面先介绍一下主要配件。</p>
<hr>
<h4 id="cpu"><a tabindex="-1" href="#cpu"><span></span></a>CPU</h4>
<p>组装 NAS 和组装台式机的过程基本一样，都是优先考虑 CPU ，再去自己搭配主板等其他配件。</p>
<p>NAS 主要还是服务于数据存储的需求，并且一般会 7x24 小时一直开机运行，以方便其他设备随时可以访问，所以更青睐于低功耗的 CPU ，毕竟不是每个人住的地方电费都是 6 毛钱（广州和深圳大部分人的电费都是 1.5 元一度，我也住过这种电费的公寓，动不动就多花不少钱，真的会心痛）。</p>
<p>对我自己来说，我的 NAS 还是存储数据为主，一些视频也没有必要上 4K 之类的（普通的 1080P 也足够我看），所以选择一个低功耗的方案性价比会更高，像赛扬 J4125 / N5105 / N100 都是蛮不错的适合 NAS 的低功耗 CPU ，但对于视频转码有需求的玩家来说，还是得上高性能的 CPU 。</p>
<p>具体有哪些合适的型号可以 Google 一下 <a href="https://www.google.com/search?q=NAS+%E4%BD%8E%E5%8A%9F%E8%80%97+CPU">NAS 低功耗 CPU</a> ，很多研究的比较深的大佬每年都会分享当下的最佳方案，所以我就不班门弄斧啦！我自己买的时候是看上了赛扬的 J4125 / N5100 / N100 这几个型号，最后是选择了 J4125 ，用下来感觉确实很不错！</p>
<hr>
<h4 id="主板"><a tabindex="-1" href="#主板"><span></span></a>主板</h4>
<p>主板通常围绕已选好的 CPU 去挑选，常见的主板规格从大到小排序分为 ATX （标准型）、 M-ATX （紧凑型）、 MINI-ITX （迷你型），尺寸越大，可拓展性越高，因此大主板通常适合 DIY 高性能主机，而用于组 NAS 的一般都是用 MINI-ITX 迷你型或者是 M-ATX 紧凑型主板。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230514224702.jpg?x-oss-process=image/interlace,1" alt="三种主板的尺寸关系">
<p>但是并非任意主板都可以和任意 CPU 配对，主板上面有 “芯片组” 来适配指定某些型号的 CPU ，芯片组是主板的核心，一块主板能提供什么样的功能、可以支持哪些处理器都是由芯片组所决定的，所以选错了会出现不兼容的情况。</p>
<p>板 U 关系对新玩家来说可能比较复杂，所以我才建议先选好合适的 CPU ，然后选购主板时就可以用 CPU 的关键词去找，例如 “J4125 ITX 主板” 这样的关键词去搜符合需求的主板。</p>
<p>确定好主板之后，其他配件也会跟着主板走，例如内存条、网卡、 USB 等插口都是和主板有关系，这也是为什么讲完 CPU 会先说主板，因为它们有一个搭配顺序。</p>
<hr>
<h4 id="内存条"><a tabindex="-1" href="#内存条"><span></span></a>内存条</h4>
<p>内存主要有四个注意点：标准、尺寸、大小、频率。</p>
<ol>
<li>传输标准</li>
</ol>
<p>传输标准指 Double Data Rate ，双倍数据速率，它的缩写我们比较熟悉，通常结合它是第几代标准，会被称为 DDR3 、 DDR4 、 DDR5 等等，它们之间的差异如下（ DDR2 以前太古老的就不放上来了）。</p>
<table>
<thead>
<tr>
<th align="center">指标</th>
<th align="center">DDR3</th>
<th align="center">DDR4</th>
<th align="center">DDR5</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">预取（预加载）</td>
<td align="center">8-bit Prefetch</td>
<td align="center">8-bit Prefetch</td>
<td align="center">16-bit Prefetch</td>
</tr>
<tr>
<td align="center">资料速度（ MT/s ）</td>
<td align="center">1066 - 1600</td>
<td align="center">2133 - 5100</td>
<td align="center">3200 - 6400</td>
</tr>
<tr>
<td align="center">传输速度（ GB/s ）</td>
<td align="center">8.5 - 14.9</td>
<td align="center">17 - 25.6</td>
<td align="center">38.4 - 51.2</td>
</tr>
<tr>
<td align="center">电压（ V ）</td>
<td align="center">1.35 - 1.5</td>
<td align="center">1.2</td>
<td align="center">1.1</td>
</tr>
</tbody>
</table>
<p>对目前主流的处理器来说，内存基本都是使用 DDR4 标准，部分比较高性能的处理器可以支持到 DDR5 ，效能更高，但对应的价格也是贵了几个档次，目前来说组 NAS 都是用 DDR4 就足够。</p>
<p>但这也不是说就无脑选 DDR4 完事，因为前面选主板时也说了，内存的插口和主板有关系，因为每一代标准的内存都会设计一个防呆槽，并且每一代的位置都不一样，因此不可以混用，买内存之前切记要看好主板可以插哪一代的内存条。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611023130.jpg?x-oss-process=image/interlace,1" alt="以台式机内存条为例，不同的 DDR 标准对应的防呆槽位置">
<p>除了防呆槽位置不同会引起无法正确插上外，内存还有尺寸也要关注，选错尺寸也会导致无法插到主板上。</p>
<blockquote>
<p>注： “防呆” 一词最初起源于日本围棋与将棋的术语，这是一种预防矫正的行为约束手段，运用避免产生错误的限制方法，让操作者不需要花费注意力、也不需要经验与专业知识即可直接无误地完成正确的操作。</p>
<p>以前的防呆设计是不兼容的接口物理形状上永远插不进去，常见的 USB 口、网线口、显示器插口都是传统的防呆设计。而现代的防呆设计理念会有更好的用户体验，例如不需要区分正反面的 Type-C ，就是一个新式的防呆设计。</p>
</blockquote>
<ol start="2">
<li>内存尺寸</li>
</ol>
<p>内存条除了 DDR4 和 DDR5 这些不同的标准带来的防呆槽在不同位置之外，内存条的造型也有一定的区别，通常会分为台式机内存条和笔记本内存条，两者的形状、尺寸、接口、安装方式都不一样，选错了也是无法安装到主板上。</p>
<p>内存条的规格有 DIMM （ Dual Inline Memory Module ，双列直插内存模块）和 SIMM （ Single In-line Memory Module ，单边接触内存模块）， SIMM 属于比较过去式的规格了，目前家用设备基本都是采用 DIMM 规格，并且根据不同的设备还有更多的子规格，常见的有以下两种：</p>
<table>
<thead>
<tr>
<th align="center">规格</th>
<th align="center">全称</th>
<th align="center">造型特点</th>
<th align="center">常用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">U-DIMM</td>
<td align="center">Unbuffered DIMM</td>
<td align="center">又瘦又长</td>
<td align="center">台式机</td>
</tr>
<tr>
<td align="center">SO-DIMM</td>
<td align="center">Small Outline DIMM</td>
<td align="center">较短、较宽</td>
<td align="center">笔记本、路由器、 NAS</td>
</tr>
</tbody>
</table>
<p>具体的造型差距如下图：</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611151827.jpg?x-oss-process=image/interlace,1" alt="台式机内存条和笔记本内存条的尺寸对比">
<p>在买内存条的时候可能会看到商品详情写着 DIMM 和 SO-DIMM ，一般 DIMM 默认为 U-DIMM 台式机内存条，而 SO-DIMM 可以默认为笔记本内存条。</p>
<p>具体需要什么尺寸规格的内存，请一定要看好主板上面可以插哪一种再去购买。</p>
<ol start="3">
<li>内存大小</li>
</ol>
<p>NAS 对内存的大小需求不高，最最入门级的存储型 NAS 选个 1-2G 内存就足够了（就真的拿来当存储用的，其他啥也不折腾），但如果要组 RAID ，或者跑 Docker 等虚拟化应用，就要视情况加内存了。</p>
<p>不过现在内存的价格也不算太贵，自己如果有打算折腾点东西，那么可以考虑直接买条 8G 的内存，实在不够再加到 16G （加内存之前要看看主板是否可以插多条内存，像 ITX 主板通常最多可以插两条内存）。</p>
<ol start="4">
<li>内存频率</li>
</ol>
<p>除了上面几个注意点之外，内存条还有一个参数需要留意，就是频率，单位是 MHz 兆赫，主频越高，内存的读写速度越快，性能越好。</p>
<p>通常 ITX 主板和对应的低压 CPU 都是推荐使用 2400MHz 的内存，所以买的时候最少要买 2400MHz 的内存（具体留意商品详情页的说明）。</p>
<p>如果购买多条内存但是同代不同频，或者是购买的是高于 CPU 支持的频率，那么会自动降频到最低的频率，例如同时使用 DDR4 2400MHz 和 DDR4 2666MHz ，实际使用会降低到 2400MHz 。</p>
<hr>
<h4 id="硬盘"><a tabindex="-1" href="#硬盘"><span></span></a>硬盘</h4>
<p>常用的硬盘主要有这两种（还有一种混合硬盘比较冷门就不提它了）；</p>
<table>
<thead>
<tr>
<th align="center">硬盘类型</th>
<th align="center">全称</th>
<th align="center">简称</th>
<th align="center">特点</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">机械硬盘</td>
<td align="center">Hard Disk Drive</td>
<td align="center">HDD</td>
<td align="center">容量大，价格便宜，但读写速度一般</td>
</tr>
<tr>
<td align="center">固态硬盘</td>
<td align="center">Solid State Drive</td>
<td align="center">SSD</td>
<td align="center">读写速度快，但价格较高</td>
</tr>
</tbody>
</table>
<p>固态虽然看起来更好，但目前价格还下不去，因此通常在预算内选购回来的容量也比较小，这也是为什么大部分 Windows 主机都是用一个小容量的 SSD 做系统盘（一般是 C 盘），搭配大容量的 HDD 做存储盘（例如 D 盘、 E 盘）。</p>
<p>组 NAS 一般都是优先考虑存储的，从性价比上来说，存储容量和价格还是推荐购买机械硬盘，另外从某些 “万一” 的情况来说，机械硬盘还可能恢复数据，固态硬盘基本都是直接没了。</p>
<p>因为本次组装 NAS 的主角是机械硬盘，所以单独在 <a href="#%E5%85%B3%E4%BA%8E%E6%9C%BA%E6%A2%B0%E7%A1%AC%E7%9B%98">关于机械硬盘</a> 部分详细展开说明，在这里只简单说一下 SSD 固态硬盘，固态硬盘的发展有这两个变化：</p>
<ul>
<li>接口：普遍从 SATA 换成了 NVMe 接口，从速度上来说 SATA 是比 NVMe 慢的</li>
<li>造型：从 2.5 英寸的造型缩小到 M.2 规格的迷你外形</li>
</ul>
<p>很多主板的详情页会说支持 M.2 NVMe SSD ，指的就是 NVMe 接口的迷你固态。</p>
<p>不过需要注意的是，接口和造型这两者并没有直接关系，事实上同时存在 NVMe M.2 SSD 和 SATA M.2 SSD ，并不是说 M.2 就一定指 NVMe SSD 。</p>
<p>另外，实际上 SSD 还有其他类型的接口，例如 mSATA 、 U.2 等等，了解起来又是一匹布那么长，因为不在常用的 NAS 搭配范围内，所以有兴趣可自行了解。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611170559.jpg?x-oss-process=image/interlace,1" alt="不同接口和造型的 SSD 对比">
<hr>
<h4 id="机箱"><a tabindex="-1" href="#机箱"><span></span></a>机箱</h4>
<p>机箱需要配合主板和硬盘位去考虑，像 ITX 主板通常就选购迷你机箱了，并且搜索关键词通常会搭配这两个配件一起搜，例如 “ITX 四盘位 机箱” 。</p>
<p>这里以某个 NAS 机箱为例，可以看到商品详情会特地注明支持什么类型的主板，还有散热风扇的尺寸</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611173304.jpg?x-oss-process=image/interlace,1" alt="NAS 四盘位机箱结构图">
<p>如果购买的是软路由一类的迷你主机来做 NAS （我这次就是选择了迷你主机，具体看后面推荐的方案一和二），那么不买机箱也可以，机械硬盘可以通过硬盘架或者硬盘笼来外置接入。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611174734.jpg?x-oss-process=image/interlace,1" alt="迷你主机结构图">
<hr>
<h4 id="散热风扇"><a tabindex="-1" href="#散热风扇"><span></span></a>散热风扇</h4>
<p>选购了机箱后，可以看看机箱里面预留的空间选择合适的散热风扇，散热风扇的选择有以下几个方面要注意：</p>
<p>1、尺寸：散热风扇的尺寸非常多，但其实并不太会有选择困难症的问题，因为风扇通常要求与主板的风扇接口大小相匹配，并且在机箱可安装的尺寸范围内，所以根据机箱要求的尺寸去购买即可</p>
<p>2、转速：风扇的转速决定了散热效果和噪音水平，通常转速越高，散热效果越好，但噪音和耗电量也会越大，所以要根据需要选择合适的转速范围</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611173458.jpg?x-oss-process=image/interlace,1" alt="可以看到通常商品购买页面有非常多的尺寸可以选择">
<p>散热风扇一般长这样，外观大同小异，主要区别还是在尺寸上：</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611173929.jpg?x-oss-process=image/interlace,1" alt="散热风扇造型参考">
<p>总之，要选择适合自己的风扇，应该根据主机机箱的大小、需要散热的硬件组件类型和数量、以及对噪音等因素的要求来进行综合考虑。</p>
<hr>
<h4 id="电源"><a tabindex="-1" href="#电源"><span></span></a>电源</h4>
<p>通常电源是放在最后选购的，因为几乎每个配件都在耗电，所以电源需要满足全部配件的总功耗，并且尺寸需要符合机箱预留的电源位。</p>
<p>常见的主机电源有 ATX 、 SFX 、 SFX-L 、 Flex 1U 电源，型号对应的体积也是从大到小排序的，自组 NAS 通常会选择 FLEX 小 1U 电源，或者根据主板要求使用 DC 供电（这里的 U 代表 Unit ，1U 等于 1.75 英寸，所以 1U 电源不全都是 FLEX 电源，小 1U 只是国内俗称）。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611184314.jpg?x-oss-process=image/interlace,1" alt="这种是 Flex 小 1U 电源">
<p>另外还有一些主板是使用 DC 供电（迷你主机就是），这种就不需要单独买一个电源了，只需要准备一个电源适配器，插到主机上就可以一直供电，这种供电方式在家庭里应该都不陌生，像我的贝斯 DI 盒就是用电源适配器供电的。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611184022.jpg?x-oss-process=image/interlace,1" alt="这种是电源适配器">
<hr>
<h4 id="参考资料"><a tabindex="-1" href="#参考资料"><span></span></a>参考资料</h4>
<ul>
<li><a href="https://post.smzdm.com/p/a4pep4rw/">NAS 折腾笔记 篇三：选择合适的设备（主板、CPU、内存）</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/99411557">芯片组和主板有啥关系？主板型号怎么看？</a></li>
<li><a href="https://diy.zol.com.cn/778/7781635.html">CPU 选购基础知识 板 U 怎么搭配？</a></li>
<li><a href="https://www.zhihu.com/question/444532170">NAS 机器的 CPU 跟内存大小对 NAS 机器运行影响有多大？</a></li>
<li><a href="https://www.crucial.tw/articles/about-memory/difference-among-ddr2-ddr3-ddr4-and-ddr5-memory">SDRAM、DDR、DDR2、DDR3、DDR4 與 DDR5 之間的差異是什麼？</a></li>
<li><a href="https://www.pcworld.com/article/558324/nvme-vs-m-2-vs-sata-ssd-whats-the-difference.html">NVMe vs. M.2 vs. SATA SSD: What’s the difference?</a></li>
<li><a href="https://www.zhihu.com/question/387154741">为什么消费级的固态硬盘很少有 u.2 接口的?</a></li>
<li><a href="https://www.fcpowerup.com/atx-sfx-sfxl-1u-flex-dimension/">ATX/SFX/SFX-L/1U Flex 尺寸区别，电源规格尺寸 v1.0</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/157678770">PC 电源也有中杯、大杯、特大杯，电源规格简单科普！顺便晒晒新入的海韵 SGX650</a></li>
</ul>
<hr>
<h3 id="关于机械硬盘"><a tabindex="-1" href="#关于机械硬盘"><span></span></a>关于机械硬盘</h3>
<p>在上面 <a href="#%E7%A1%AC%E7%9B%98">硬盘</a> 部分已经说过 NAS 优先考虑购买机械硬盘，但挑选机械硬盘也不是单纯的一句 “我要机械硬盘” 就完事，选购机械硬盘通常还会遇到这两类问题：</p>
<ol>
<li>磁记录方式上：分有 CMR （常规磁记录）和 SMR （叠瓦式磁记录）</li>
<li>外观和尺寸上：分有 3.5 英寸（台式机）和 2.5 英寸（笔记本）</li>
</ol>
<p>另外还有接口和转速要怎么选的小问题。</p>
<hr>
<h4 id="磁记录区别"><a tabindex="-1" href="#磁记录区别"><span></span></a>磁记录区别</h4>
<p>常规磁记录（ Conventional Magnetic Recording ，简称 CMR ），俗称 “垂直盘” ，垂直盘的优点是在写入数据的时候不会出现其他多余的操作，写入速度快，硬盘寿命长；缺点是单碟容量略低。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611190217.jpg?x-oss-process=image/interlace,1" alt="CMR 的磁道示例">
<p>叠瓦式磁记录（ Shingled Magnetic Recording ，简称 SMR ），俗称 “叠瓦盘” ，叠瓦盘的优点是单碟容量更大，造价成本低所以价格也相对便宜（好像也区别不大）；缺点则是当存了太多数据后，即使微小的数据改动，也会反复迁移扇区内的数据，这将严重影响写入速度，并加速盘片和磁头的老化，数据丢失的风险也随之增加。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611190218.jpg?x-oss-process=image/interlace,1" alt="SMR 的磁道示例">
<p>对于 NAS 用户来说，通常会存储非常多的珍贵数据，在 “数据无价” 四个字面前，叠瓦盘带来的容量和价格优势，相比性能下降和数据丢失风险较高来说并不算值得考虑的点，所以可以看到很多人都说买盘别买叠瓦盘。</p>
<p>所以如果用来做仓库盘，例如存放电影、音乐、文件备份这种平时放着不用、偶尔才选一些出来看，类似这些读写频率不高的场景，可以考虑 SMR 叠瓦盘，否则都应该优先考虑 CMR 垂直盘（如果看到标记了 PMR/CMR 也不用担心， PMR 是 CMR 技术的前身）。</p>
<p>如果商品详情没有标记，也可以看缓存大小来简单判断，一般 64M 缓存或以下的都是 CMR ，而 256M 之类的大缓存硬盘基本上都是 SMR （非绝对，但是相对靠谱）。</p>
<hr>
<h4 id="尺寸区别"><a tabindex="-1" href="#尺寸区别"><span></span></a>尺寸区别</h4>
<p>机械硬盘分有 3.5 英寸和 2.5 英寸两种规格，对于 NAS 机箱来说，通常都是以 3.5 英寸来设计的，外置硬盘座或者硬盘笼也是一样，如果手里有 2.5 英寸的硬盘，需要注意是否可以兼容使用。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611191901.jpg?x-oss-process=image/interlace,1" alt="两者的区别">
<p>不过 2.5 英寸机械硬盘基本上都是 SMR 叠瓦盘，所以…… 我是冇得拣，只考虑 3.5 英寸的机械硬盘。</p>
<hr>
<h4 id="接口"><a tabindex="-1" href="#接口"><span></span></a>接口</h4>
<p>目前硬盘接口主流使用的是 SATA 、 mSATA 和 M.2 接口，另外还有一些是 PCIe 接口（较少）。</p>
<p>M.2 接口目前主要运用给迷你固态硬盘（ NMVe SSD ）， mSATA 也是一种比较小的固态硬盘（我现在主机里就用了一个 mSATA 的 SSD 做系统盘），而 SATA 则是机械硬盘和普通的 2.5 英寸固态硬盘使用。</p>
<p>SATA 现在又分为 SATA 2 （又称 SATA 3G ）和 SATA 3 （又称 SATA 6G ），主要区别在传输率， SATA 3 的传输速率可以达到 6 Gb/s ，而 SATA 2 接口的传输速率则为 3Gb/s ，理论上 SATA 3 接口是 SATA 2 的两倍。</p>
<p>所以购买机械硬盘的时候优先考虑 SATA 3 。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/06/20230611192549.jpg?x-oss-process=image/interlace,1" alt="几种不同的接口参考">
<p>但现在也有外置硬盘座，是可以使用 USB 3.0 来代替 SATA 口和主板连接，传输速度不会差太多，不过不适合所有人，最好确认自己家里是否会有外力因素去碰到它（例如你家的猫会不会上桌 - - ），或者是容易发生地震之类自然灾害的地区，硬盘座需要有很稳定的外界空间才可以考虑。</p>
<hr>
<h4 id="转速"><a tabindex="-1" href="#转速"><span></span></a>转速</h4>
<p>机械硬盘常见的转速一般有 5400 转、 5900 转、 7200 转，硬盘的转速越高，读写的速度就越快，但是相对的，噪音也更大一些，一般来说 NAS 选用 5400 转也够用。</p>
<hr>
<h4 id="硬盘容量"><a tabindex="-1" href="#硬盘容量"><span></span></a>硬盘容量</h4>
<p>这里就不说一共需要多少个 T 的问题了，只讨论是买几个盘。比如京东二手上面西数 500G 的二手盘才 39 块钱，而一样是西数的 2T 二手盘却要 ￥ 219 ，在有一个四盘位的机箱，并且需求是 2T 容量的情况下，是不是买 4 个 500G 的比买一个 2T 的划算？</p>
<p>当然不是，因为每增加一个硬盘，耗电量就会增加，所以尽量每个盘都是越大越好，虽然只看硬盘的耗电不算太大，但是 NAS 通常都是 7x24h 持续运行，加上其他的配件消耗每一个都多一点的话，在广州和深圳地区，除了住小区可以享受 6 毛钱一度的电费，其他地方都是 1.5 元一度电，这么贵的电费长时间算下来，可能真的很不划算。</p>
<hr>
<h4 id="参考资料-1"><a tabindex="-1" href="#参考资料-1"><span></span></a>参考资料</h4>
<ul>
<li><a href="https://zhuanlan.zhihu.com/p/57800929">SMR 瓦楞式堆叠磁盘是什么东西，和传统磁盘有啥区别</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/320796902">这些硬盘不要买！2023 那些不推荐的机械硬盘</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/528801946">垂直盘和叠瓦盘区别？叠瓦盘为什么不推荐？2023 年机械硬盘购买推荐</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/544175438">2023 年，HDD 机械硬盘推荐，尽量不选叠瓦盘</a></li>
<li><a href="https://www.sohu.com/a/553078949_120111001">为什么说「买盘别买叠瓦盘」？一文带你看懂 SMR、CMR 的区别</a></li>
<li><a href="https://www.sohu.com/a/580279682_121118998">一文看懂硬盘的主要接口和传输速度</a></li>
<li><a href="https://blog.csdn.net/shuai0845/article/details/98330290">SATA、mSATA、M.2、M.2（NVMe）、PCIE 固态硬盘接口详解</a></li>
</ul>
<hr>
<h2 id="搭配方案"><a tabindex="-1" href="#搭配方案"><span></span></a>搭配方案</h2>
<p>在确定最终组装方案之前，自己考虑了三个方向的方案如下，其中价格暂时都是以淘宝和京东的价格作为参考，实际购买的时候可以去拼多多比价。</p>
<p>尺寸的单位都是 “毫米” ，顺序是 “长 x 宽 x 高” 或者 “长 x 宽” 。</p>
<blockquote>
<p>补充说明一下：</p>
<p>这里的方案一二三都是调研阶段整理的，在这里就不改了，我自己实际的最终搭配见最前面的 <a href="#%E6%9C%80%E7%BB%88%E7%BB%84%E8%A3%85%E6%96%B9%E6%A1%88">最终组装方案</a> 。</p>
<p>另外这里时 2023 到 2024 期间的推荐，再往后请自己搭配了，毕竟硬件会过时。</p>
</blockquote>
<h3 id="方案一迷你--外置"><a tabindex="-1" href="#方案一迷你--外置"><span></span></a>方案一：迷你 + 外置</h3>
<ul>
<li>搭配说明：自选迷你主机 + 硬盘笼 + 若干个 3.5 英寸的机械硬盘</li>
<li>主要优点：存储容量大（外挂机械硬盘），机箱迷你低功耗，搭配省事</li>
<li>一些缺点：拖个硬盘笼或者硬盘支架，怎么看怎么丑</li>
<li>适合对象：刚入门 NAS ，还懵懵懂懂，但有一定存储需求的玩家</li>
<li>千元入门：见下表</li>
</ul>
<p>先列一下需要的配件和价格：</p>
<table>
<thead>
<tr>
<th align="center">硬件</th>
<th align="left">链接</th>
<th align="center">价格</th>
<th align="left">尺寸</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">主机</td>
<td align="left">见下方搭配</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">主板</td>
<td align="left">主机包了</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">电源</td>
<td align="left">通常购买主机有送</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">风扇</td>
<td align="left">主机可以不用，但硬盘笼要搭配一个</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">硬盘笼</td>
<td align="left"><a href="https://item.taobao.com/item.htm?id=688559998268">4 盘位 3.5 英寸机械硬盘笼 + 1 个风扇</a></td>
<td align="center">￥ 33.8</td>
<td align="left">185 x 110 x 180</td>
</tr>
<tr>
<td align="center">硬盘</td>
<td align="left"><a href="https://item.taobao.com/item.htm?id=706447703621">日立机械硬盘 3.5 英寸 / 4T / CRM / SATA3 / 7200 转 / 64M 缓存</a></td>
<td align="center">￥ 195 x 2</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">内存条</td>
<td align="left"><a href="https://item.taobao.com/item.htm?id=558603479364">英特尔专用 DDR4 8G 2666MHz</a> （记得备注发三星颗粒）</td>
<td align="center">￥ 65</td>
<td align="left">-</td>
</tr>
</tbody>
</table>
<p>配件总价格：￥ 489 ，如果觉得配件的价格占比偏高，可以适当调整硬盘方案，例如改成 2 个 1T ，或者是小黄鱼或者京东二手买二手盘。</p>
<p>目前选的日立虽然早已被西数收购变成 HGST 系列，据说现在卖的价格亲民的都是拆机盘，但查了一下好像质量还可以，故障率非常低，这个预算也不奢求太多，我感觉可以买来试试。</p>
<p>如果选购热门牌子像西数、希捷等，一定要注意查好型号是 SMR 还是 CMR ，很多详情页都不会显式告知是不是 CMR ，那么大概率是 SMR 。</p>
<p>再看看主机的选择和方案总价：</p>
<table>
<thead>
<tr>
<th align="center">主机</th>
<th align="center">尺寸</th>
<th align="center">功耗</th>
<th align="center">价格</th>
<th align="center">方案总价</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><a href="https://detail.tmall.com/item.htm?id=688206851233">畅网 J4105 标准板 准系统</a></td>
<td align="center">112.3 x 100 x 36.5</td>
<td align="center">6W</td>
<td align="center">￥ 568</td>
<td align="center">￥ 1057</td>
</tr>
<tr>
<td align="center"><a href="https://detail.tmall.com/item.htm?id=663382260464">倍控 G40-J4125 准系统</a></td>
<td align="center">137 x 127 x 40</td>
<td align="center">-</td>
<td align="center">￥ 580</td>
<td align="center">￥ 1069</td>
</tr>
<tr>
<td align="center"><a href="https://detail.tmall.com/item.htm?id=688206851233">畅网 J4125 标准板 准系统</a></td>
<td align="center">112.3 x 100 x 36.5</td>
<td align="center">6W</td>
<td align="center">￥ 628</td>
<td align="center">￥ 1117</td>
</tr>
<tr>
<td align="center"><a href="https://detail.tmall.com/item.htm?id=663382260464">倍控 G30B-N5105 准系统</a></td>
<td align="center">137 x 127 x 40</td>
<td align="center">-</td>
<td align="center">￥ 680</td>
<td align="center">￥ 1169</td>
</tr>
<tr>
<td align="center"><a href="https://detail.tmall.com/item.htm?id=712232085836">零刻 EQ12 N100 准系统</a></td>
<td align="center">124 x 113 x 39</td>
<td align="center">25W</td>
<td align="center">￥ 795</td>
<td align="center">￥ 1284</td>
</tr>
<tr>
<td align="center"><a href="https://detail.tmall.com/item.htm?id=669578613219">畅网 N5105-i226 V5 准系统</a></td>
<td align="center">178.3 x 135.6 x 55</td>
<td align="center">10W</td>
<td align="center">￥ 883</td>
<td align="center">￥ 1372</td>
</tr>
<tr>
<td align="center"><a href="https://detail.tmall.com/item.htm?id=714619184058">畅网 N100 准系统</a></td>
<td align="center">145.6 x 145.4 x 53.6</td>
<td align="center">15W</td>
<td align="center">￥ 968</td>
<td align="center">￥ 1457</td>
</tr>
</tbody>
</table>
<p>关于畅网和倍控，据说畅网贵一些的原因是畅网的机箱散热和电源会好一些，差价也基本都差在机箱和电源上。</p>
<h3 id="方案二纯迷你主机"><a tabindex="-1" href="#方案二纯迷你主机"><span></span></a>方案二：纯迷你主机</h3>
<ul>
<li>搭配说明：自选迷你主机 + 1 个迷你固态</li>
<li>主要优点：主打各种迷你，低功耗，搭配省事，颜值一般也高</li>
<li>一些缺点：存储容量一般（仅用一个固态硬盘）</li>
<li>适合对象：刚入门 NAS ，还懵懵懂懂，并且存储需求不大的玩家</li>
<li>千元入门：见下表</li>
</ul>
<p>在方案一的基础上，把机械硬盘换成 NVMe 的固态硬盘即可，整体的价格方面会相对方案一低一些。</p>
<p>但这个方案因为走了 SSD 存储，所以可能需要关注一下本身硬盘速度是否有影响。</p>
<h3 id="方案三自组全部"><a tabindex="-1" href="#方案三自组全部"><span></span></a>方案三：自组全部</h3>
<ul>
<li>搭配说明：自选主板 + 带硬盘笼的机箱 + 若干个 3.5 英寸的机械硬盘</li>
<li>主要优点：存储容量大（大容量机械硬盘便宜），机箱通常比较帅（也可以自己选择 3D 打印）</li>
<li>一些缺点：比较笨重（例如机箱重大 4.26kg ，加上其他的硬盘、风扇，可能高达 10 至 15 斤），成本也较高</li>
<li>适合对象：有玩硬件的经验，虽然刚入门 NAS ，但搭配起来不费劲的老司机</li>
<li>千元入门：见下表</li>
</ul>
<p>暂无时间推荐，根据前面基础知识部分的内容去搜符合要求的配件。</p>
<table>
<thead>
<tr>
<th align="center">硬件</th>
<th align="left">链接</th>
<th align="center">价格</th>
<th align="left">尺寸</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">机箱</td>
<td align="left">支持 ITX 主板的四盘位机箱</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">主板</td>
<td align="left">搜索 ITX 主板配合一个高性价比的 CPU</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">电源</td>
<td align="left">选购一个 FLEX 小 1U 电源</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">风扇</td>
<td align="left">根据 CPU 和硬盘需要搭配选购</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">硬盘</td>
<td align="left">通常两个或者四个 CMR 机械硬盘</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
<tr>
<td align="center">内存条</td>
<td align="left">根据主板要求选条 DDR4 8G 内存</td>
<td align="center">-</td>
<td align="left">-</td>
</tr>
</tbody>
</table>
<p>写到这里好累，周末两天就过去了， 系统的安装现在推荐直接用飞牛 fnOS ，官方有很靠谱的说明，开箱即用基本没问题。除了系统安装之外，还有 Docker 、 Clash 代理 、基于 FRP 和 Cloudflare Tunnel 的两套内网穿透方案部署等等（飞牛现在很多都内置支持了），很多东西也可以玩一玩。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[我写了一本书《前端工程化：基于 Vue.js 3.0 的设计与实践》 想分享一下它背后的故事]]></title>
            <link>https://chengpeiquan.com/article/the-story-of-the-book-learning-vue3</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/the-story-of-the-book-learning-vue3</guid>
            <pubDate>Sat, 06 May 2023 14:03:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>大家好，我是程沛权，经过差不多一年时间的打磨和优化，我的第一本技术书籍《前端工程化：基于 Vue.js 3.0 的设计与实践》出版上市啦！</p>
<img src="https://cdn.chengpeiquan.com/img/2025/01/202501012318823.jpg?x-oss-process=image/interlace,1" alt="前端工程化：基于 Vue.js 3.0 的设计与实践">
<p>这是一本以 Vue.js 的 3.0 版本为核心技术栈，围绕 “前端工程化” 和 TypeScript 的知识点展开讲解的前端入门书籍，主要面向以下读者人群：</p>
<ol>
<li>掌握了基础的 HTML 页面编写知识，想学习一个主流前端框架的新手前端工程师</li>
<li>已经学会了 Vue 2 ，面对 Vue 3 的大版本更新，想快速上手使用的前端工程师</li>
<li>非职业前端开发，但涉及前端的工作，需要掌握一个主流前端框架的全栈工程师</li>
</ol>
<p>书里面的知识点是按照工程师做项目的顺序梳理的，比较循序渐进的一个过程，读者可以收获到这些知识：</p>
<ol>
<li>了解如何入门前端工程化开发，掌握 Node.js 和 npm 的使用</li>
<li>掌握前端领域多年来趋势走高、带有类型支持的 TypeScript 语言</li>
<li>上手主流前端框架 Vue.js 的全新版本，并且在遇到常见问题时知道如何解决</li>
</ol>
<p>看到这里，我估计有部分读者会觉得眼熟，有熟悉的感觉是对的！因为在它被正式出版之前，有另外一个名字是叫《Vue3 入门指南与实战案例》，最早是部署在我的博客网站上作为本书配套案例和代码资源的 <a href="https://vue3.chengpeiquan.com/">开源版本</a> 分享的。</p>
<p>截止至 2023-05-03 五一假期的最后一天，开源版本已经累计了大约 220 万的阅读人次，受到了不少读者朋友的关注和支持，所以我相信很多人是看过它的在线版本，上面这段话其实就是前言里面的一部分。</p>
<p>今天这篇文章更主要是想分享一下这本书的由来，还有一些关于我、关于写书这些事背后的一些故事，希望感兴趣的读者可以继续支持！</p>
<h2 id="目录详情和购买地址"><a tabindex="-1" href="#目录详情和购买地址"><span></span></a>目录详情和购买地址</h2>
<p>先放上纸质书的购买地址吧，关于书的目录和内容介绍可以在商品详情查看：</p>
<p>☞ 访问 <a href="https://union-click.jd.com/jdc?e=jdext-1638352360249409536-0-1&#x26;p=JF8BARkJK1olXwQBUVpdAE8SAF8IGVMRXgICV24ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYAXFpeDEsUHDZNRwYlX1JEIQg5XCt0ZAlqUiVNX0FQACsWTkcbM28BG1kdXAcCU11tCEoWA2sNGFgTXDYyVFttWiXPtdnQvuoJiayNgdbKOEonA2gBGVkdXwEHVFdeAXsXC2s4zfWBiI69je743uG51uK4ztK-ibiEZG5tC3tMVjtBXkcVWgQLVlpeCkwWAGoAHlodWQQFSF9BCHsXAm4KE1gWWgQGOltdCUoVAGoBElh7XwcDVltdDksXAV8IK1glA2gDB1heWkweVgFVQgkdX00BHTBdDUgXAW4OHF8lXwcDVlxtOA">京东商城</a> 购买</p>
<p>☞ 访问 <a href="https://s.click.taobao.com/t?e=m%3D2%26s%3D7eZPjJt7QJNw4vFB6t2Z2ueEDrYVVa64yK8Cckff7TVRAdhuF14FMXonIcf4DInoMMgx22UI05ZRvxcz%2FoTyBGNojKDSmWQhTIZbYI9jayp8PNk4B98QhUVUjoeWqzb%2B5mzd0fxoCIaFpjofm3hpRhwogYNSK3IrQPZdAhulFAULZMqoQW%2BfuB6GmlJyRiVTGSs8kMDMeyhHJZaVw28y8Wrk5fXXieVy6a%2F%2FENJP36wMyM3sDoIE3myjwLjQ1DptJJn7FkCnT%2F5j0n6UXAgF3v2%2BtTfElYb8RFDRgzhUg%2BSjJnAf507Uv%2BiK7LcQp3KospWd4zfY8zqcEdwGxwkp5JaYzSMEwlxThakdowOjbI2OmjxjH%2BEyQGTWRvaaxnzAXJHdUWjDlfMDnfwHPQnOXiGFCzYOOqAQ&#x26;unid=1X3aISuD7B60&#x26;union_lens=lensId:TAPI@1683471818@2103de44_0d37_187f6bd6062_8bdf@01">天猫商城</a> 购买</p>
<p>如果您对我的作品认可，建议购买纸质版，纸质书在电子书的基础上，经过机械工业出版社的编辑老师们的内容优化、校对勘误、排版美化，更成体系，在此特别感谢李晓波编辑对我将开源作品出版为纸质作品的支持，李老师全程帮忙跟进了无数的大事小事，也给我科普了很多出版方面的知识，十分尽职！</p>
<h2 id="这本书怎么样"><a tabindex="-1" href="#这本书怎么样"><span></span></a>这本书怎么样？</h2>
<p>在出版纸质书之前，电子版就开通了基于 GitHub Issue 的评论功能，收到了很多读者给我的反馈和交流，摘选了一部分如下（排名不分时间先后）：</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230505011051.jpg?x-oss-process=image/interlace,1" alt="GitHub 上的读者评论">
<p>也有来自宝岛的小姐姐热心安利（我就说某段时间突然有很多来自台湾的工程师关注，然后 Google 了一下发现了这个推，感谢我的热心网友们！</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230505011723.jpg?x-oss-process=image/interlace,1" alt="来自宝岛的小姐姐热心安利">
<p>因为之前有热心读者问我有没有赞赏渠道，所以我从去年 10 月份在文档上挂了一个赞赏码，可以给我家三只猫猫打赏点罐头，也收到了很多读者的捐赠，有好几笔是大额的赞赏，特别特别感谢！！！</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230505014847.jpg?x-oss-process=image/interlace,1" alt="一些赞赏记录">
<p>所以关于这本书的内容质量，我相信您看到这里时应该也有了一个大概的了解！</p>
<p>放上我家三只猫，欢迎在线吸猫！</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230505015055.jpg?x-oss-process=image/interlace,1" alt="我家的三只猫">
<img src="https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1" alt="我家的三只猫">
<h2 id="这本书是怎么写出来的"><a tabindex="-1" href="#这本书是怎么写出来的"><span></span></a>这本书是怎么写出来的？</h2>
<p>作为写了一本大约 32 万字的成品书籍的作者，我一开始并没有很功利的想把它写成一本书，它最早最早的前身其实是我无数日夜在学习过程中的笔记碎片。</p>
<p>刚开始关注和使用 Vue 3 的时候还是 2020 年，那段时间资料很少，相关的官网也只是基于 Vue 2 的内容向上适配了一下，并且只有英文版，其他的随手可得的资料，真的好少好少…… 我那段时间也算处于一个比较早期的开荒阶段，遇到问题也只能 Google 和 StackOverflow ，还有在 GitHub 仓库的 Issue 区和源码挖挖看有没有解决方案。</p>
<p>再后来发现还是不太够用，慢慢地又跑去 <a href="https://github.com/vuejs/rfcs">RFC 仓库</a> 挖掘一些还没有正式公开，但其实已经实现的 “隐藏功能” ，期间也整理过一些文章分享出来过（例如： <a href="https://zhuanlan.zhihu.com/p/386919557">Vue3.0 最新动态：script-setup 定稿 部分实验性 API 将弃用</a> ， Evan 大佬还在评论区亲自帮忙解答大家的问题）。</p>
<p>因为开荒的过程都是利用本就不多的休息时间，所以早期为了解决各种问题和先把项目做出来，也没有第一时间整理博客笔记，我通常的习惯都是先把找到的资料丢给我一个专门存档临时笔记的微信小号，作为一个可检索的 “便签” 。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230505225018.png?x-oss-process=image/interlace,1" alt="当时记录的一些临时笔记">
<p>再后来发现实在太多了，这么零散的记录我自己回头也容易忘记，秉着对费曼学习法的多次实践（用输出来倒逼输入，真的特别有效！），逐步整理出了第一个版本，在 2020 年国庆节那会部署到了博客上面，刚上线的时候它还只是一本很纯粹的关于 Vue 3 的入门学习指南，内容比较单一，阅读门槛也比较高（需要本身已经熟悉了 Vue 2 才能看懂我在说什么）。</p>
<p>很感谢第一波读者的鼓励，很热心地给了我评论反馈，还收到了很多邮件交流遇到的问题，经过不断地迭代，慢慢地补充了很多通俗易懂的例子，并且逐步增加了关于前端工程化和 TypeScript 的入门学习内容。</p>
<p>期间还有很多读者自发帮我宣传（ e.g. <a href="https://github.com/EbookFoundation/free-programming-books/pull/6075">PR #6075</a> ），让我写的内容逐步被更多的人看到，所以到了后面一共有三家出版社联系了我出版事宜，也才有了后面这些出版相关的事情（因为收到不同出版社的邀请在时间上跨度比较大，所以我也是选择了最早联系我的李晓波编辑和机械工业出版社，在这里也很感谢其他出版社的编辑对我的作品的认可！）。</p>
<h2 id="我是一个什么样的人"><a tabindex="-1" href="#我是一个什么样的人"><span></span></a>我是一个什么样的人？</h2>
<p>再来说说我自己，我并不是一个很纯粹的程序员。</p>
<p>认识我久一点的朋友也知道我并不是从毕业就一直在做开发，虽然读书那会选了网络专业也勉强算是科班出身（哈哈哈哈我真的不太想提及这些往事，因为那个时候自己也还挺不懂事，老在挂科边缘徘徊，经常这边老师教完知识，过段时间我又还给老师了，反正就是成绩不怎么好的那一类人），相对于做开发或者运维等更重技术的岗位，我那个时候更喜欢打游戏、混各种论坛社区写一些挂机脚本、喜欢看人家怎么做产品设计等东西，所以毕业后跑去游戏行业做了产品运营（我在 UC 的那两年就是做产品运营）。</p>
<p>一开始倒是觉得做运营挺有趣的，但后来发现我的运营 KPI 很大程度依赖于在重要节点的各种活动和宣传合作页面用于推广，但 “拿不到排期” 这个事情总是在阻碍着我达成 KPI ，后来我就跟技术部的同事发起了一个小小的请求：“一些简单的需求页面我自己写，你们有空的时候能不能稍微帮我审查下代码和安排部署？” ，然后我就开始在工作时间干起了写代码的活，我负责的运营业务的数据指标也因为需求能如期上线而基本能按预期跑满，从此就一发不可收拾。</p>
<p>但那段时间我还不敢把自己定位为程序员去换工作，毕竟学的越多越觉得自己渺小，计算机的世界里有太多自己没有接触到的东西，所以 2015 年从 UC 跳槽去网易游戏的时候，我还是去面的运营岗位，进入了大话手游业务线，那段时间刚好面临大话手游即将上线，已经在计划中的需求 1234 是真的多，但又是喜闻乐见的 “没有排期了” ，所以入职后作为一个运营，安排给我的第一个任务是写一个答题器，我就……（此处是 Max 的问号脸.jpg ）</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506000311.jpg?x-oss-process=image/interlace,1" alt="Max 的问号脸.jpg">
<p>其实是我当时的运营简历也写了我会写页面，在网易的面试过程中也聊到了我在 UC 自己做自己的需求的经历，所以当时的网易总监也知道我可以写…… 经过 “一直需求顺利上线一直爽” 的阶段后，再后来就在部门内就成立了自己的技术组、产品组，开始有了自己的前端开发和产品策划等不同岗位，所以从 2016 年开始我就开始正式以写代码为生了，到 2023 年的今年，刚好 7 年。</p>
<p>兜兜转转从技术专业跑去做产品运营，再从运营又杀回来做技术，经常有人问我那几年后不后悔，也有人很好奇我转回技术这段过程难不难。</p>
<p>先说关于难不难，我只想说两个字：“热爱” ！当你对一件事情有了足够的热爱，真的没有什么难的。这里的 “热爱” 指的是可以长期保持数年的喜欢不变心，而不是三分钟热度的 “教练我要学” 盲目跟风。</p>
<p>再说说关于后不后悔这件事，至少目前的 7 年里是没有后悔的，我本身从小就不是一个按部就班的人，总是跟着我的兴趣去做我喜欢的事情，青少年时期我的同学都喜欢看球、喜欢听流行歌曲、喜欢唱情歌，而我那个时候就已经是一个享受着孤独沉迷在自己世界里的听摇滚乐的人，从来不看球，从来不听情歌，至于在其他人眼里可能显得比较独来独往和特立独行，从小好像就不是特别在意。</p>
<p>我家境很一般，在我工作之前，我爸爸妈妈每天忙碌养家也只是赚个温饱，所以我基本上是没有零用钱这种东西，虽然早在初中高中那会就想玩乐队，但吉他贝斯等乐器和学琴的费用在当时真的是完全消费不起，但也没有妨碍我对它们一直保持热爱，买不起我就自己做，自己画版型，自己锯木料，自己上油漆，给自己做了最喜欢的 BEYOND 乐队吉他手阿 Paul 在 1991 年演唱会上的那把斯坦伯格的无头吉他模型满足自己内心的 Rock'N Roll ，这可能是我最早期的自己的需求自己实现吧啊哈哈哈。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506001707.jpg?x-oss-process=image/interlace,1" alt="自己做的吉他模型">
<p>工作后身边的同龄人开始恋爱结婚生子学车考驾照，我是完全反着来，因为不喜欢坐车和极少出门，所以我到现在也没有想考驾照的念头，反而在爸妈眼里的 “你也老大不小了（后半句就是你该成家了…… ）” 的年纪才开始自学弹琴、组乐队玩演出圆我当初的乐手梦，当年第一把琴还是借的，只想着过把瘾，结果发现真的放不下了，到了后面自己也走上了买自己喜欢的琴的不归路。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506013700.png?x-oss-process=image/interlace,1" alt="左边是借的第一把贝斯，右边是自己的琴">
<p>再后来有了一些志同道合的伙伴一起夹 Band ，有幸也登上了几次三千多人的舞台，在别人都是唱流行歌曲和谐氛围里，我们又玩起了相对小众的新金属，哪怕现在我同学孩子都已经很大了，我还每天沉迷在 “塞狗、发克鹰、馊兽” 这样的嘶吼音乐里（这是 Slipknot 活结乐队在 <a href="https://music.163.com/#/song?id=21616345">Psychosocial</a> 这首歌的某次现场版开场，特别燥的一个 Live 版）……</p>
<p>对于爱好和生活之间的平衡，这一点我特别佩服中山大学的何广平教授，白天衬衫教书，晚上甩头演出，双面人生太让我羡慕了！（可戳： <a href="https://zhuanlan.zhihu.com/p/29220064">一边是量子物理，一边是极端金属，中大教授诠释科研与音乐的完美融合</a> 了解何教授）。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506010210.png?x-oss-process=image/interlace,1" alt="用了好久 “胸口碎大石” 这个乐队名">
<p>头发也是从 2018 年初就再也没有去剪过，从光头到莫西干再到现在的长发及腰，我感觉我还可以继续留下去……</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506010544.png?x-oss-process=image/interlace,1" alt="“程小姐” 你好…">
<p>最让我走上 “只过自己喜欢的生活” 的不归路就是下定决心去文身了，从不到 20 岁的时候就想要纹一条花臂，也是念念不忘了多年后终于开始了行动，第一个文身是我的琴，然后是我的猫，然后是音乐元素，还有一些逆境阶段的有意义的图，有一些是自己设计点草图，到了后面默契上来了就全部交给我的专属文身师 Johnny 了，从 2016 年文身到现在，还在继续加图案，每一个图案都在自己的人生阶段里有着不同的意义。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506010724.png?x-oss-process=image/interlace,1" alt="我的花臂">
<p>说了这些往事，主要还是想回到前面说的，只要保持足够的 “热爱” ，很多困难真的不是困难，而且因为热爱，也会自然而然的遇到一群志同道合的好朋友，很感谢这么多年一路陪我玩陪我成长以及在我转行过程中给到了很多帮助的同事好友！</p>
<p>如果对我的更多往事感兴趣，可以扫码查看我之前整理的我在网易五年的工作和生活的记录。</p>
<img src="https://cdn.chengpeiquan.com/img/2020/06/my-netease/qrcode.jpg?x-oss-process=image/interlace,1" alt="记录一下在网易五年来的工作与生活">
<h2 id="表达能力如何培养"><a tabindex="-1" href="#表达能力如何培养"><span></span></a>表达能力如何培养？</h2>
<p>最后分享一个可能会比较多程序员都比较关心的问题，如何锻炼自己的码字能力和表达能力。虽然我很宅，不爱社交，但很多读者在阅读了我的文字之后给我的反馈都是看完容易理解，性格和表达似乎出现了矛盾？其实不然。</p>
<p>我之前写过一篇文章叫 <a href="https://zhuanlan.zhihu.com/p/460538277">Markdown 工程师的一周</a> ，分享过我曾经一周写了大约 25 个小时的 Markdown 文档，几乎一天有 5 个小时在码字。</p>
<img src="https://cdn.chengpeiquan.com/img/2022/01/20220121232856.png?x-oss-process=image/interlace,1" alt="一周写了 24 多个小时的文档">
<p>为什么我不觉得写文档是个很费劲的事情？因为我自己常常保持的习惯有主要两个：</p>
<p>一个是保持每天写日记的习惯，把自己的喜怒哀乐都记录到日记里，开心的时候记录起来以后可以回顾，不开心的时候用文字把负面情绪输出走，这个方式可以很好的让自己不会把坏情绪带回家里或者带入到工作中，从几个字到两三千字我都写过，内容的多和少并不需要很刻意的去要求，重要的是这一天对你来说，过的有没有意义，我从 2019 年把写日记的习惯重新培养起来到现在也有 4 年了，已经连续写了 4 年日记，没有中断过。</p>
<p>另外一个习惯是上面说的费曼学习法，把自己学到的东西输出成博客文章或者其他什么载体都可以（例如下面的两个 PPT ，是我之前在一些分享会上的演讲稿，已脱敏），一开始写分享内容时可能会觉得无从下手，但是如果能够长期保持习惯，久而久之自己所写的东西就会开始有了逻辑有了条理。</p>
<ul>
<li><a href="https://requirement-design.ppt.chengpeiquan.com">六步写出有效的需求文档 从技术的角度分析：产品需求如何 “以德服人”</a></li>
<li><a href="https://hoo-retrospective-meeting.ppt.chengpeiquan.com">前端技术复盘 面对停运项目重启复活时的思考与落地</a></li>
</ul>
<p>因为上面两个习惯，让我后来开始记录我平时做菜时的菜谱也蛮多人爱看。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506222320.png?x-oss-process=image/interlace,1" alt="左边是我的小红书账号，右边是我曾经写了两千多字的日记">
<h2 id="写在最后"><a tabindex="-1" href="#写在最后"><span></span></a>写在最后</h2>
<p>能看到这里的朋友对我真的是真爱了，希望我的书也能给到您一些帮助，希望您在继续学习的路上，或者转行的路上，都可以实现自己想达到的目标！</p>
<p>再次附上买书链接：</p>
<p>☞ 访问 <a href="https://union-click.jd.com/jdc?e=jdext-1638352360249409536-0-1&#x26;p=JF8BARkJK1olXwQBUVpdAE8SAF8IGVMRXgICV24ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYAXFpeDEsUHDZNRwYlX1JEIQg5XCt0ZAlqUiVNX0FQACsWTkcbM28BG1kdXAcCU11tCEoWA2sNGFgTXDYyVFttWiXPtdnQvuoJiayNgdbKOEonA2gBGVkdXwEHVFdeAXsXC2s4zfWBiI69je743uG51uK4ztK-ibiEZG5tC3tMVjtBXkcVWgQLVlpeCkwWAGoAHlodWQQFSF9BCHsXAm4KE1gWWgQGOltdCUoVAGoBElh7XwcDVltdDksXAV8IK1glA2gDB1heWkweVgFVQgkdX00BHTBdDUgXAW4OHF8lXwcDVlxtOA">京东商城</a> 购买</p>
<p>☞ 访问 <a href="https://s.click.taobao.com/t?e=m%3D2%26s%3D7eZPjJt7QJNw4vFB6t2Z2ueEDrYVVa64yK8Cckff7TVRAdhuF14FMXonIcf4DInoMMgx22UI05ZRvxcz%2FoTyBGNojKDSmWQhTIZbYI9jayp8PNk4B98QhUVUjoeWqzb%2B5mzd0fxoCIaFpjofm3hpRhwogYNSK3IrQPZdAhulFAULZMqoQW%2BfuB6GmlJyRiVTGSs8kMDMeyhHJZaVw28y8Wrk5fXXieVy6a%2F%2FENJP36wMyM3sDoIE3myjwLjQ1DptJJn7FkCnT%2F5j0n6UXAgF3v2%2BtTfElYb8RFDRgzhUg%2BSjJnAf507Uv%2BiK7LcQp3KospWd4zfY8zqcEdwGxwkp5JaYzSMEwlxThakdowOjbI2OmjxjH%2BEyQGTWRvaaxnzAXJHdUWjDlfMDnfwHPQnOXiGFCzYOOqAQ&#x26;unid=1X3aISuD7B60&#x26;union_lens=lensId:TAPI@1683471818@2103de44_0d37_187f6bd6062_8bdf@01">天猫商城</a> 购买</p>
<p>如果想问我接下来会做什么？我也是继续保持学习哈哈哈哈哈哈！我在今年三月份刚刚换了新工作，来到了一个很牛逼的团队，技术氛围特别好，队友们的能力也好强，未来也有很多我之前没有接触的东西，我也还在继续成长！</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506015009.jpg?x-oss-process=image/interlace,1" alt="之前跟朋友说的哈哈哈">
<p>最后的最后，分享一下我保持学习的动力和精力的秘诀……</p>
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506015141.jpg?x-oss-process=image/interlace,1" alt="一年多前觉得是这个原因">
<img src="https://cdn.chengpeiquan.com/img/2023/05/20230506015142.jpg?x-oss-process=image/interlace,1" alt="现在还是觉得是这个原因">
<p>谢谢您的支持！</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[Git的选择性合并操作笔记：合并某个版本或某个提交]]></title>
            <link>https://chengpeiquan.com/article/git-selective-merge</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/git-selective-merge</guid>
            <pubDate>Thu, 02 Feb 2023 08:56:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>今天帮朋友解决了一个代码合并的问题，他有两个项目， B 项目最初是基于 A 项目作为架构底子，根据业务进行了不同需求的开发，沉淀了不少新功能，而 A 项目本身也在继续维护，可以简单的理解为， A 项目是通过类似 <a href="https://github.com/awesome-starter/create-preset">create-preset</a> 这样的脚手架拉取下来的一个项目模板，而 B 项目是一个业务项目，所以 A 项目通常只提供一些公共功能的维护升级，而 B 项目更注重业务功能开发。</p>
<table>
<thead>
<tr>
<th align="center">项目</th>
<th align="left">作用</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">A 项目</td>
<td align="left">基础模板，提供一些公共功能的维护升级</td>
</tr>
<tr>
<td align="center">B 项目</td>
<td align="left">业务项目，在 A 项目的基础上，围绕具体业务进行功能开发</td>
</tr>
</tbody>
</table>
<p>因此 B 项目想升级 A 项目的功能，并不是不可能，冲突也不一定会很多。不过因为不是每个版本都升级，所以如果手动合并代码，工作量又比较大。分享了几个方案给他尝试，当然这些操作对我来说也不怎么常用，所以记录起来，以后自己用到也不用重新踩坑。</p>
<p>下面的演示都基于同一个 Git 仓库的不同分支，实际处理中，可以选择把 B 项目放到 A 项目的某个分支来实现合并，也可以通过 <code>git remote add &#x3C;shortname> &#x3C;url></code> ，将两个项目关联到同一个 Git 远程仓库。</p>
<h2 id="合并某个版本"><a tabindex="-1" href="#合并某个版本"><span></span></a>合并某个版本</h2>
<p>先看看一个版本合并的操作，假设 B 项目不想完全升级到 A 项目的最新版本（可能因为最新版本是一个包含破坏性升级的 Major 版本，出现了很多不兼容的情况），那么可以选择其中一个 Minor 次要版本进行升级。</p>
<h3 id="处理思路"><a tabindex="-1" href="#处理思路"><span></span></a>处理思路</h3>
<p>由于并不是每个项目都有打 Tag ，所以可以选择指定某个 Commit Hash 作为临界点。在这个例子里，将 A 项目在某个 commit 记录之前的功能，都需要同步给 B 项目，这里使用 <code>git merge &#x3C;commit_hash></code> 命令来实现。</p>
<h3 id="具体操作"><a tabindex="-1" href="#具体操作"><span></span></a>具体操作</h3>
<p>随便拉取了一个旧的版本，拉取深度为 <code>10</code> （主要参数是 <code>--depth 10</code> ，其他的 <code>--branch</code> 和 <code>--single-branch</code> 可以视情况指定，这里只是为了单纯拉取 <code>main</code> 分支的代码）。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> clone</span><span style="color:#50A14F;--shiki-dark:#CE9178"> https://github.com/chengpeiquan/test.git</span><span style="color:#986801;--shiki-dark:#569CD6"> --branch</span><span style="color:#50A14F;--shiki-dark:#CE9178"> main</span><span style="color:#986801;--shiki-dark:#569CD6"> --single-branch</span><span style="color:#986801;--shiki-dark:#569CD6"> --depth</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 10</span></span></code></pre>
<p>进入仓库文件夹，使用 <code>git log</code> 命令查看本地仓库和远程仓库的 <code>main</code> 分支差异（这里使用了 <code>--oneline</code> 参数简化了提交记录的展示）。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#0184BC;--shiki-dark:#DCDCAA">cd</span><span style="color:#50A14F;--shiki-dark:#CE9178"> test</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> log</span><span style="color:#986801;--shiki-dark:#569CD6"> --oneline</span><span style="color:#50A14F;--shiki-dark:#CE9178"> main</span><span style="color:#50A14F;--shiki-dark:#CE9178"> origin/main</span></span></code></pre>
<p>看一下 Log ，确实是 10 条 Commit 差异：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># git log --oneline main origin/main</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">3ad0947</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (HEAD -> </span><span style="color:#50A14F;--shiki-dark:#CE9178">main,</span><span style="color:#50A14F;--shiki-dark:#CE9178"> origin/main,</span><span style="color:#50A14F;--shiki-dark:#CE9178"> origin/HEAD</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">fc322fe</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1cc6c01</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">4898c9c</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">8258834</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">cb7d560</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">10b3bb5</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">2119218</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1c0c5ba</span><span style="color:#50A14F;--shiki-dark:#CE9178"> feat:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">f49e8b0</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (grafted) chore: backup</span></span></code></pre>
<p>将本地的 Commit 记录重置到拉取时的那个 Commit ，如果不执行这一步，在 Merge 的时候会提示 <code>Already up-to-date.</code> 的信息，无法合并。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> reset</span><span style="color:#986801;--shiki-dark:#569CD6"> --hard</span><span style="color:#50A14F;--shiki-dark:#CE9178"> f49e8b0</span></span></code></pre>
<p>在 10 条 Commit 差异里，现在使用 <code>git merge &#x3C;commit_hash></code> 命令合并第五条 Commit （它的 Hash 是 <code>cb7d560</code> ），保留 5 条差异。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> merge</span><span style="color:#50A14F;--shiki-dark:#CE9178"> cb7d560</span></span></code></pre>
<p>合并成功反馈：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># git merge cb7d560</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Updating</span><span style="color:#50A14F;--shiki-dark:#CE9178"> f49e8b0..cb7d560</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Fast-forward</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">README.md</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> | </span><span style="color:#4078F2;--shiki-dark:#DCDCAA">14</span><span style="color:#50A14F;--shiki-dark:#CE9178"> +++++++++++---</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">docs/introduction.md</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> | </span><span style="color:#4078F2;--shiki-dark:#DCDCAA">1</span><span style="color:#50A14F;--shiki-dark:#CE9178"> -</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">docs/zh/introduction.md</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> | </span><span style="color:#4078F2;--shiki-dark:#DCDCAA">1</span><span style="color:#50A14F;--shiki-dark:#CE9178"> -</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">3</span><span style="color:#50A14F;--shiki-dark:#CE9178"> files</span><span style="color:#50A14F;--shiki-dark:#CE9178"> changed,</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 11</span><span style="color:#50A14F;--shiki-dark:#CE9178"> insertions</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">+</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span><span style="color:#50A14F;--shiki-dark:#CE9178">,</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 5</span><span style="color:#50A14F;--shiki-dark:#CE9178"> deletions</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">-</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">delete</span><span style="color:#50A14F;--shiki-dark:#CE9178"> mode</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 100644</span><span style="color:#50A14F;--shiki-dark:#CE9178"> docs/introduction.md</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">delete</span><span style="color:#50A14F;--shiki-dark:#CE9178"> mode</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 100644</span><span style="color:#50A14F;--shiki-dark:#CE9178"> docs/zh/introduction.md</span></span></code></pre>
<p>再次查看本地 <code>main</code> 分支和远程 <code>main</code> 分支的差异：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> log</span><span style="color:#986801;--shiki-dark:#569CD6"> --oneline</span><span style="color:#50A14F;--shiki-dark:#CE9178"> main</span><span style="color:#50A14F;--shiki-dark:#CE9178"> origin/main</span></span></code></pre>
<p>本地的 HEAD 已经在第五条了：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># git log --oneline main origin/main</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">3ad0947</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (origin/main, </span><span style="color:#50A14F;--shiki-dark:#CE9178">origin/HEAD</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">fc322fe</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1cc6c01</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">4898c9c</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">8258834</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">cb7d560</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (HEAD -> </span><span style="color:#50A14F;--shiki-dark:#CE9178">main</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">10b3bb5</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">2119218</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1c0c5ba</span><span style="color:#50A14F;--shiki-dark:#CE9178"> feat:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">f49e8b0</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (grafted) chore: backup</span></span></code></pre>
<p>为了避免覆盖，创建一个新分支来提交刚刚本地合并后的代码：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> checkout</span><span style="color:#986801;--shiki-dark:#569CD6"> -b</span><span style="color:#50A14F;--shiki-dark:#CE9178"> dev</span></span></code></pre>
<p>再次检查一下新分支 <code>dev</code> 分支和远程 <code>main</code> 分支的差异：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> log</span><span style="color:#986801;--shiki-dark:#569CD6"> --oneline</span><span style="color:#50A14F;--shiki-dark:#CE9178"> dev</span><span style="color:#50A14F;--shiki-dark:#CE9178"> origin/main</span></span></code></pre>
<p>确认没有问题（因为是从本地 <code>main</code> 分支创建的，所以代码是一样的，但在进行敏感操作之前，还是养成一个二次检查的习惯）。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># git log --oneline dev origin/main</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">3ad0947</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (origin/main, </span><span style="color:#50A14F;--shiki-dark:#CE9178">origin/HEAD</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">fc322fe</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1cc6c01</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">4898c9c</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">8258834</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">cb7d560</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (HEAD -> </span><span style="color:#50A14F;--shiki-dark:#CE9178">dev,</span><span style="color:#50A14F;--shiki-dark:#CE9178"> main</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">10b3bb5</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">2119218</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1c0c5ba</span><span style="color:#50A14F;--shiki-dark:#CE9178"> feat:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">f49e8b0</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (grafted) chore: backup</span></span></code></pre>
<p>可以提交到远程的 <code>dev</code> 分支了（也就是实际上 B 项目代码所在的分支）。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> push</span><span style="color:#986801;--shiki-dark:#569CD6"> -u</span><span style="color:#50A14F;--shiki-dark:#CE9178"> origin</span><span style="color:#50A14F;--shiki-dark:#CE9178"> dev</span></span></code></pre>
<img src="https://cdn.chengpeiquan.com/img/2023/02/20230203163624.jpg?x-oss-process=image/interlace,1" alt="main 分支的提交记录">
<img src="https://cdn.chengpeiquan.com/img/2023/02/20230203163625.jpg?x-oss-process=image/interlace,1" alt="dev 分支的提交记录">
<h2 id="合并某个提交"><a tabindex="-1" href="#合并某个提交"><span></span></a>合并某个提交</h2>
<p>有时候仅仅想单独合并某一条或者某几条 Commit 的改动，不希望包含该 Commit 之前的其他 Commit ，可以选择这个方案来处理。</p>
<h3 id="处理思路-1"><a tabindex="-1" href="#处理思路-1"><span></span></a>处理思路</h3>
<p>与 <a href="#%E5%90%88%E5%B9%B6%E6%9F%90%E4%B8%AA%E7%89%88%E6%9C%AC">合并某个版本</a> 的操作不同，这种情况会产生新的 Commit Hash ，因此整个提交历史会被打乱，所以比较适合简单的代码合并，例如某个模块新增了功能，而其他的功能并不需要，只想要这个模板的新功能，那么选择这个方案会比较合适。</p>
<p>这个方案就不再是使用 <code>git merge</code> 了，而是使用 <code>git cherry-pick &#x3C;commit_hash></code> 命令来实现。</p>
<h3 id="具体操作-1"><a tabindex="-1" href="#具体操作-1"><span></span></a>具体操作</h3>
<p>继续在 <code>dev</code> 分支上操作，也是先查看差异：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> log</span><span style="color:#986801;--shiki-dark:#569CD6"> --oneline</span><span style="color:#50A14F;--shiki-dark:#CE9178"> dev</span><span style="color:#50A14F;--shiki-dark:#CE9178"> origin/main</span></span></code></pre>
<p>由于前面 <a href="#%E5%90%88%E5%B9%B6%E6%9F%90%E4%B8%AA%E7%89%88%E6%9C%AC">合并某个版本</a> 的操作是合并第五个 Commit 以及之前的提交记录，因此还有 5 个未同步的记录。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># git log --oneline dev origin/main</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">3ad0947</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (origin/main, </span><span style="color:#50A14F;--shiki-dark:#CE9178">origin/HEAD</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">fc322fe</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1cc6c01</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">4898c9c</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">8258834</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">cb7d560</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (HEAD -> </span><span style="color:#50A14F;--shiki-dark:#CE9178">dev,</span><span style="color:#50A14F;--shiki-dark:#CE9178"> main</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">10b3bb5</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">2119218</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1c0c5ba</span><span style="color:#50A14F;--shiki-dark:#CE9178"> feat:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">f49e8b0</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (grafted) chore: backup</span></span></code></pre>
<p>这里挑选未同步的记录里面的一个，只合并这个：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> cherry-pick</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 4898c9c</span></span></code></pre>
<p>合并成功反馈：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># git cherry-pick 4898c9c</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Auto-merging</span><span style="color:#50A14F;--shiki-dark:#CE9178"> README.md</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">[dev 3f8b616] chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Date:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Mon</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Dec</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 5</span><span style="color:#50A14F;--shiki-dark:#CE9178"> 13:54:43</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 2022</span><span style="color:#50A14F;--shiki-dark:#CE9178"> +0800</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1</span><span style="color:#50A14F;--shiki-dark:#CE9178"> file</span><span style="color:#50A14F;--shiki-dark:#CE9178"> changed,</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 5</span><span style="color:#50A14F;--shiki-dark:#CE9178"> insertions</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">+</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span><span style="color:#50A14F;--shiki-dark:#CE9178">,</span><span style="color:#986801;--shiki-dark:#B5CEA8"> 1</span><span style="color:#50A14F;--shiki-dark:#CE9178"> deletion</span><span style="color:#383A42;--shiki-dark:#D4D4D4">(</span><span style="color:#4078F2;--shiki-dark:#DCDCAA">-</span><span style="color:#383A42;--shiki-dark:#D4D4D4">)</span></span></code></pre>
<p>现在重新查看本地 <code>dev</code> 分支和远程 <code>main</code> 分支的差异，可以看到多出来一个新的提交了。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># git log --oneline dev origin/main</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">3f8b616</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (HEAD -> </span><span style="color:#50A14F;--shiki-dark:#CE9178">dev</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">3ad0947</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (origin/main, </span><span style="color:#50A14F;--shiki-dark:#CE9178">origin/HEAD</span><span style="color:#383A42;--shiki-dark:#D4D4D4">) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">fc322fe</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1cc6c01</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">4898c9c</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">8258834</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">cb7d560</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (main) chore: backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">10b3bb5</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">2119218</span><span style="color:#50A14F;--shiki-dark:#CE9178"> chore:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">1c0c5ba</span><span style="color:#50A14F;--shiki-dark:#CE9178"> feat:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> backup</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">f49e8b0</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> (grafted) chore: backup</span></span></code></pre>
<h2 id="其他补充"><a tabindex="-1" href="#其他补充"><span></span></a>其他补充</h2>
<p>在演示项目上操作通常还是很顺利的，但实际项目里可能会存在代码冲突，建议在操作时，按照小版本的提交去合并同步，减少冲突的解决成本。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[年终总结：2022 年的一些回顾和 2023 年的一些小规划]]></title>
            <link>https://chengpeiquan.com/article/2022-year-end-summary</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/2022-year-end-summary</guid>
            <pubDate>Sat, 31 Dec 2022 13:24:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>2022 对我算是比较特殊的一年，虽然因为疫情原因彻底宅了一年，但也没有闲着，换了工作换了城市，回到了阔别两年的第二故乡广州，工作之外的学习和生活对个人的成长也有一些值得复盘的地方。</p>
<h2 id="工作与生活"><a tabindex="-1" href="#工作与生活"><span></span></a>工作与生活</h2>
<p>2020 年疫情爆发那年发生了很多事情，最终也决定从服务了五年的网易公司离职（见 <a href="https://chengpeiquan.com/topic/netease">记录一下在网易五年来的工作与生活</a>），随即和几个朋友跟着大佬去了深圳，因为大家都是扎根广州，所以都把深圳当成一个临时过渡的城市，所以今年时机成熟的时候就又一起回到了广州。</p>
<p>回广州后也终于不用再苟在城中村里了，在依山傍水的山景房里，三只猫也迎来了久违的阳光，每天睡到自然醒，醒了有东西吃有玩具玩，还不用出门工作，过的比我还幸福。</p>
<video src="https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4" poster="https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1" title="山景房里的三只猫" controls preload="metadata" class="w-full aspect-video rounded-lg"><p><a href="https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4">https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4</a><a href="https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1">https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1</a>山景房里的三只猫</p></video>
<p>我从五月底回来后就一直很忙，因为广州这边也是刚成立的新公司，太多事情要忙，加上业余时间也有其他事情， <a href="https://chengpeiquan.com/cookbook">菜谱</a> 也慢慢停更了，不过偶尔还是有在做饭，新的一年看看调整下生活节奏重新运营起来。</p>
<p>虽然工作很忙碌，但在初创公司的日子里倒也是很有趣，因为人少（目前加上老板还不到十五个人），所以自己也有机会开始承担起一些前端工程师之外的开发工作，例如写 App ，写客户端，写服务端，写爬虫，写区块链，当然都是围绕着 Node.js 进行的全栈开发。</p>
<p>写着写着就没有什么时间写页面了，所以今年也是把 Tailwind CSS 这一类原子框架大幅度的往工作项目里用，这样在需要写页面的时候可以不怎么写样式了，对 Flex 布局熟悉的前端工程师来说， Tailwind 风格的开发模式做东西真的非常快，很适合初创公司老板说 “啊啊啊这个东西很着急” 然后第二天就可以给到他。</p>
<p>这一年是真的忙碌（见下文的 <a href="#%E5%BC%80%E6%BA%90%E7%A4%BE%E5%8C%BA">开源社区</a> 部分），以至于几乎没有什么运动量，可能也因为运动太少，所以从九月份开始，从痛风发作到新冠病毒，陆陆续续当了一个季度的病号，不过有一点算是不幸中的万幸，就是因为一直宅着，所以年底新冠阳性的时候仅仅只是个弱阳，低烧了一个晚上就开始在恢复健康了，相对于在朋友圈看到一些朋友病得五颜六色，我还是挺幸运的。</p>
<p>另外关于生活的话，就是头发达到了历史上的最新长度，往后甩的时候已经过了腰了，虽然发质一如既往的差，但我并不关心发质哈哈哈哈，长度才是王道。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101231241.jpg?x-oss-process=image/interlace,1" alt="头发达到了新的长度">
<h2 id="开源社区"><a tabindex="-1" href="#开源社区"><span></span></a>开源社区</h2>
<p>今年在 GitHub 的活跃度比去年高了不少，而且很神奇的达到了全勤，因为疫情的原因，还有换城市和新工作等事情，今年也没有出远门或者回家待几天，每天都在有电脑的屋子里待着，所以每天都有在 GitHub 上操作（截图生成自 <a href="https://github-contributions.vercel.app/">GitHub Contributions</a> ），也归功于今年把很多有的没的代码都托管到 GitHub 上了，包括一些 to-do list 也会有个 Markdown 仓库作为临时笔记，很方便哈哈哈。</p>
<img src="https://cdn.chengpeiquan.com/img/2022/12/20221231235848.jpg?x-oss-process=image/interlace,1" alt="这两年在 GitHub 的活跃情况">
<p>虽然这么活跃，但开源项目其实没有参与多少，基本上都在维护去年的一些旧项目，其中投入最多的是之前写的那本关于 Vue 3 的书 <a href="https://github.com/chengpeiquan/learning-vue3">Learning Vue3</a> 。</p>
<p>今年一月份的时候在知乎收到一条私信，是出版社的编辑发现了我写的东西，邀请我参与到书籍的出版工作中。当时由于临近过年比较忙，没有立即答复是否参加，后面四月份编辑老师又联系了我，在考虑了自己的时间和目前手里头比较有积累有篇幅的内容后，咨询了是否可以用现有的开源内容整理出版（还举了阮一峰老师的 <a href="https://es6.ruanyifeng.com">ES6 入门教程</a> 的例子），得到了肯定的答复之后，就开始安排时间调整 Learning Vue3 的内容。</p>
<p>由于当时 Vue 3 已经成为 npm 安装时的默认版本，并且中文官网也建设起来了，考虑到继续单独讲述 Vue 3 的内容显得有点 “过时” （毕竟当时写这本书的背景是国内没有什么 Vue 3 的资料，并且还处于公测但未完全替代 Vue 2 的阶段），在出版社编辑老师的建议下，续写了关于前端工程化的内容，以降低本书的学习门槛，所以可以看到我从 <a href="https://vue3.chengpeiquan.com/changelog.html#_2022-04-30">四月底的更新记录</a> 就慢慢不再局限于 Vue 了，而是一直在更新前端工程化的内容。</p>
<p>再之后的几个月就一直在补充新内容，并且按照出版社的供稿规范调整了叙述风格，最终整个文档的风格就变得比较有 “专业性” ，不再是嘻嘻哈哈的逗逼语气，自己感觉有点像阮一峰老师，确实在阮老师的博客里受到了很大影响。</p>
<p>很高兴续写的内容得到了线上读者的认可，不仅仓库从 100 Star 慢慢涨到了现在接近 500 Star ，还有很多读者给我留言，给了我很大的鼓励！</p>
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101232239.jpg?x-oss-process=image/interlace,1" alt="仓库接近 500 Star 了">
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101223359.jpg?x-oss-process=image/interlace,1" alt="在 GitHub 仓库里的评论">
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101223358.jpg?x-oss-process=image/interlace,1" alt="在 GitHub 仓库里的评论">
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101223357.jpg?x-oss-process=image/interlace,1" alt="在 GitHub 仓库里的评论">
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101223356.jpg?x-oss-process=image/interlace,1" alt="在 GitHub 仓库里的评论">
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101223355.jpg?x-oss-process=image/interlace,1" alt="在 GitHub 仓库里的评论">
<p>调整后的叙述风格也被读者认可，在 2022 年底突然有一群台湾开发者关注了我的 GitHub 账号，搜索了一下才发现原来 <a href="https://www.facebook.com/mukispace/posts/pfbid02QNhZR5DkCAZvtkyJWJuHSSBtW2qPtoUJajQo59sRQbAG2zyRUqcskwGNPxcCEHB3l">被台湾一位技术大 V 推荐了</a> ，她很喜欢我的风格，我很开心哈哈哈，付出得到了回报，得到了认可！</p>
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101224000.jpg?x-oss-process=image/interlace,1" alt="台湾网友的分享">
<p>其他的开源项目大部分是一些 npm 包，总下载量也突破 35 万次了（数据来源 <a href="https://npm-stat.com/charts.html?author=chengpeiquan&#x26;from=2019-12-31&#x26;to=2022-12-31">npm-stat</a> ）。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101224519.jpg?x-oss-process=image/interlace,1" alt="来自 npm-stat 网站的统计数据">
<p>有个 Vite 插件被大佬的项目引入后带火了，现在每周大约保持在 7k 左右的下载量，也收到了其他开发者的 PR 贡献（ <a href="https://github.com/chengpeiquan/vite-plugin-banner/pull/16">#16</a> ），期间认真的评审了代码和给予了意见点评，直到那天才感觉在认真参与开源项目协作，在此之前都是自己单打独斗，或者发起一些没有什么修改意见的翻译 PR ，很少这样在 PR 过程中讨论功能的取舍。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101225852.jpg?x-oss-process=image/interlace,1" alt="在 PR 过程中讨论功能的取舍">
<p>还有一个使用量比较多的插件是一年多前做的一个自用的小工具，很久没维护，结果一直有人用，前段时间还提了一些反馈，所以年底有空于是重新维护了起来，还用 Photoshop 鼠绘了个 Logo ，偶尔设计点东西还挺有意思的，虽然渣渣，但我觉得很贴合用途哈哈哈。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101230308.jpg?x-oss-process=image/interlace,1" alt="新官网上挂着自己画的 Logo">
<p>另外还有一点比较值得高兴的是，新开坑的开源仓库都已经全部使用英语了，包括代码里的注释，还有 issue 的交流，虽然语法和用词什么的不一定对，但在 Google 翻译校验了一下还是可以识别的，多读多写多练习，总有一天可以不太依赖翻译工具的。</p>
<h2 id="新年展望"><a tabindex="-1" href="#新年展望"><span></span></a>新年展望</h2>
<p>刚好很多事情都在 2022 年底弄完了，新的一年业余时间应该不会那么忙了，我要多运动，多爬山，明明现在住的地方对面就是一座山，我居然一次都没有爬过，真是浪费啊浪费可耻哈哈哈。</p>
<img src="https://cdn.chengpeiquan.com/img/2023/01/20230101231242.jpg?x-oss-process=image/interlace,1" alt="底迪很喜欢在阳台看山景">
<p>然后把三年没弹的贝斯重新弹起来，太久没有弹琴了，自从去了深圳，总感觉人在漂泊，以至于从 2020 年之后就没有再弹过琴，希望今年可以重新积累一些可以演奏的曲子。</p>
<p>技术方面，长远的规划不好说，梳理了一下最近几个月的计划吧：</p>
<ol>
<li>重构博客（上一次是 2020 年 1 月，也有两年了），技术选型还是偏向于比较新的前端技术栈，找个时间调研一下</li>
<li>认真学一下 React ，其实很久前就有想法，但因为缺乏可落地的个人项目，所以一直没动手，只是简单的写了几个 demo ，去年写 App 的时候因为主要技术栈还是 Vue ，所以选了 uni-app ，但感觉体验并不是太好，把 React 玩熟悉之后以后也可以切入 React Native 开发</li>
<li>重构之前写的命令行工具 <a href="https://github.com/awesome-starter/create-preset">create-preset</a> ，一个模板拉取工具，现在在用的 GitHub 镜像挂掉了一直没去修，打算把一些自己维护的模板处理成本地一起发包，不走 GitHub 下载了，其他人的社区模板就继续拉仓库，不过要考虑现有的 API 兼容，只能是无感知的重构，感觉要做的事情也不少</li>
</ol>
<p>其他的可能就是 Web 3 方面的开发继续深入吧，智能合约和 dApp ，也是未来的趋势，并且基本上都是以前端的技术栈为主，作为前端工程师，天生有前端优势的情况下，不玩下区块链多可惜！</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[知乎收藏夹助手：自动化将专栏的文章添加到收藏夹]]></title>
            <link>https://chengpeiquan.com/article/zhihu-collection-helper</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/zhihu-collection-helper</guid>
            <pubDate>Fri, 21 Oct 2022 15:53:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>知乎收藏夹助手，可将专栏的文章添加到收藏夹中（因为专栏的内容通常比较垂直，不会太杂），适用于收藏夹在创建初期需要大量内容填充的情况。</p>
<p>参考收藏夹：<a href="https://www.zhihu.com/collection/839257512">凶杀案·刑侦与法医</a></p>
<h2 id="使用方法"><a tabindex="-1" href="#使用方法"><span></span></a>使用方法</h2>
<p>这是一个 Node.js 项目，因此需要提前安装 <a href="https://nodejs.org/zh-cn/">Node.js</a> 。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 克隆仓库到本地</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">git</span><span style="color:#50A14F;--shiki-dark:#CE9178"> clone</span><span style="color:#50A14F;--shiki-dark:#CE9178"> https://github.com/chengpeiquan/zhihu-collection-helper.git</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 进入项目目录</span></span>
<span class="line"><span style="color:#0184BC;--shiki-dark:#DCDCAA">cd</span><span style="color:#50A14F;--shiki-dark:#CE9178"> zhihu-collection-helper</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 安装依赖（可使用 npm / yarn 等其他包管理器）</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">pnpm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> i</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit"># 运行开始命令</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">pnpm</span><span style="color:#50A14F;--shiki-dark:#CE9178"> start</span></span></code></pre>
<p>不过程序正确运行的前提是需要先准备好一个 <a href="#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">配置文件</a> 。</p>
<h2 id="配置文件"><a tabindex="-1" href="#配置文件"><span></span></a>配置文件</h2>
<p>基于 <a href="https://github.com/motdotla/dotenv">dotenv</a> 管理配置文件，创建一个名为 <code>.env</code> 的文件保存到根目录（与 src 目录同级别）。</p>
<p>注意所有的配置项的值都是字符串，也就是使用双引号括住。</p>
<table>
<thead>
<tr>
<th align="center">配置选项</th>
<th align="center">作用</th>
<th align="center">数据来源或设置说明</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">COLLECTION_ID</td>
<td align="center">要添加数据的收藏夹 ID</td>
<td align="center">收藏夹的 URL</td>
</tr>
<tr>
<td align="center">DATA_SOURCE_COLLECTION_ID</td>
<td align="center">要爬取的收藏夹 ID</td>
<td align="center">收藏夹的 URL</td>
</tr>
<tr>
<td align="center">START_PAGE_NUMBER</td>
<td align="center">起始的爬取页数</td>
<td align="center">如果中间</td>
</tr>
<tr>
<td align="center">LIMIT</td>
<td align="center">分页条数限制</td>
<td align="center">上限 <code>100</code></td>
</tr>
<tr>
<td align="center">X_AB_PB</td>
<td align="center">请求的鉴权参数</td>
<td align="center">知乎 AJAX 请求的 <code>Request Headers</code></td>
</tr>
<tr>
<td align="center">X_XSRFTOKEN</td>
<td align="center">请求的鉴权参数</td>
<td align="center">知乎 AJAX 请求的 <code>Request Headers</code></td>
</tr>
<tr>
<td align="center">X_ZSE_93</td>
<td align="center">请求的鉴权参数</td>
<td align="center">知乎 AJAX 请求的 <code>Request Headers</code></td>
</tr>
<tr>
<td align="center">X_ZSE_96</td>
<td align="center">请求的鉴权参数</td>
<td align="center">知乎 AJAX 请求的 <code>Request Headers</code></td>
</tr>
<tr>
<td align="center">X_ZSE_81</td>
<td align="center">请求的鉴权参数</td>
<td align="center">知乎 AJAX 请求的 <code>Request Headers</code></td>
</tr>
<tr>
<td align="center">COOKIE</td>
<td align="center">请求的鉴权参数</td>
<td align="center">知乎 AJAX 请求的 <code>Request Headers</code></td>
</tr>
</tbody>
</table>
<h2 id="其他说明"><a tabindex="-1" href="#其他说明"><span></span></a>其他说明</h2>
<p>因为操作太快会导致行为限制（返回 <code>403 Forbidden</code> ），所以每次请求之前都通过 <a href="./src/utils.ts#L36-L43">sleep</a> 方法进行一次随机秒数的睡眠，可在这里调整操作间隔。</p>
<p>如果账号被限制了，等几个小时后再试就可以了。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[用Vite更简单的解决Vue3项目的预渲染问题]]></title>
            <link>https://chengpeiquan.com/article/vite-vue3-prerender</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/vite-vue3-prerender</guid>
            <pubDate>Thu, 02 Jun 2022 06:59:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>之前 Webpack 项目经常会用到预渲染，现在团队都开始用 Vite 了，所以弄一个基于 Vite 的 Vue 3 预渲染 demo 可以参考。</p>
<p>预渲染和静态生成器比较接近，也可以参考我的 <a href="https://github.com/chengpeiquan/chengpeiquan.com/blob/main/src/router/index.ts">SSG 博客</a> 用 <a href="https://www.npmjs.com/package/vite-ssg">vite-ssg</a> 和 <a href="https://www.npmjs.com/package/vite-plugin-pages">vite-plugin-pages</a> 来处理。</p>
<p>不过 Vite 本身对预渲染也提供了原生的支持，简单的预渲染可以自己写写代码来改造实现。</p>
<h2 id="html-部分"><a tabindex="-1" href="#html-部分"><span></span></a>HTML 部分</h2>
<p>项目根目录下 <code>index.html</code> 里需要追加至少两条资源注入位置的注释：</p>
<table>
<thead>
<tr>
<th align="center">注释语句</th>
<th align="center">作用</th>
<th align="center">是否必须</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><code>&#x3C;!--preload-links--></code></td>
<td align="center">预加载资源</td>
<td align="center">是</td>
</tr>
<tr>
<td align="center"><code>&#x3C;!--app-html--></code></td>
<td align="center">页面内容</td>
<td align="center">是</td>
</tr>
<tr>
<td align="center"><code>&#x3C;!--title--></code></td>
<td align="center">SEO 优化：写入标题</td>
<td align="center">否</td>
</tr>
<tr>
<td align="center"><code>&#x3C;!--description--></code></td>
<td align="center">SEO 优化：写入描述</td>
<td align="center">否</td>
</tr>
<tr>
<td align="center"><code>&#x3C;!--keywords--></code></td>
<td align="center">SEO 优化：写入关键词</td>
<td align="center">否</td>
</tr>
</tbody>
</table>
<p>并把入口文件改成 <code>entry-client.ts</code> ，原来的 <code>main.ts</code> 会作为客户端和服务端启动时的引用。</p>
<p>完整代码如下（源码：<a href="https://github.com/chengpeiquan/vite-vue3-prerender-demo/blob/main/index.html">index.html</a> ）：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-html"><span class="line"><span style="color:#383A42;--shiki-dark:#808080">&#x3C;!</span><span style="color:#E45649;--shiki-dark:#569CD6">doctype</span><span style="color:#986801;--shiki-dark:#9CDCFE"> html</span><span style="color:#383A42;--shiki-dark:#808080">></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">&#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">html</span><span style="color:#986801;--shiki-dark:#9CDCFE"> lang</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"en"</span><span style="color:#383A42;--shiki-dark:#808080">></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">  &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">head</span><span style="color:#383A42;--shiki-dark:#808080">></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">    &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">meta</span><span style="color:#986801;--shiki-dark:#9CDCFE"> charset</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"UTF-8"</span><span style="color:#383A42;--shiki-dark:#808080"> /></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">    &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">link</span><span style="color:#986801;--shiki-dark:#9CDCFE"> rel</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"icon"</span><span style="color:#986801;--shiki-dark:#9CDCFE"> href</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"/favicon.ico"</span><span style="color:#383A42;--shiki-dark:#808080"> /></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">    &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">meta</span><span style="color:#986801;--shiki-dark:#9CDCFE"> name</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"viewport"</span><span style="color:#986801;--shiki-dark:#9CDCFE"> content</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"width=device-width, initial-scale=1.0"</span><span style="color:#383A42;--shiki-dark:#808080"> /></span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">    &#x3C;!--title--></span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">    &#x3C;!--description--></span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">    &#x3C;!--keywords--></span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">    &#x3C;!--preload-links--></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">  &#x3C;/</span><span style="color:#E45649;--shiki-dark:#569CD6">head</span><span style="color:#383A42;--shiki-dark:#808080">></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">  &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">body</span><span style="color:#383A42;--shiki-dark:#808080">></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">    &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">div</span><span style="color:#986801;--shiki-dark:#9CDCFE"> id</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"app"</span><span style="color:#383A42;--shiki-dark:#808080">></span><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">&#x3C;!--app-html--></span><span style="color:#383A42;--shiki-dark:#808080">&#x3C;/</span><span style="color:#E45649;--shiki-dark:#569CD6">div</span><span style="color:#383A42;--shiki-dark:#808080">></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">    &#x3C;</span><span style="color:#E45649;--shiki-dark:#569CD6">script</span><span style="color:#986801;--shiki-dark:#9CDCFE"> type</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"module"</span><span style="color:#986801;--shiki-dark:#9CDCFE"> src</span><span style="color:#383A42;--shiki-dark:#D4D4D4">=</span><span style="color:#50A14F;--shiki-dark:#CE9178">"/src/entry-client.ts"</span><span style="color:#383A42;--shiki-dark:#808080">>&#x3C;/</span><span style="color:#E45649;--shiki-dark:#569CD6">script</span><span style="color:#383A42;--shiki-dark:#808080">></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">  &#x3C;/</span><span style="color:#E45649;--shiki-dark:#569CD6">body</span><span style="color:#383A42;--shiki-dark:#808080">></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#808080">&#x3C;/</span><span style="color:#E45649;--shiki-dark:#569CD6">html</span><span style="color:#383A42;--shiki-dark:#808080">></span></span></code></pre>
<p>其中除了两条必须的注释语句外，可选的部分见 <a href="#seo-%E4%BC%98%E5%8C%96">SEO 优化</a> 。</p>
<h2 id="入口文件"><a tabindex="-1" href="#入口文件"><span></span></a>入口文件</h2>
<p>普通项目是使用 <code>src/main.ts</code> 作为入口文件，需要改造成两个入口：</p>
<table>
<thead>
<tr>
<th align="center">注释语句</th>
<th align="center">作用</th>
<th align="center">源码</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><code>entry-client.ts</code></td>
<td align="center">客户端入口</td>
<td align="center"><a href="https://github.com/chengpeiquan/vite-vue3-prerender-demo/blob/main/src/entry-client.ts">查看源码</a></td>
</tr>
<tr>
<td align="center"><code>entry-server.ts</code></td>
<td align="center">服务端入口</td>
<td align="center"><a href="https://github.com/chengpeiquan/vite-vue3-prerender-demo/blob/main/src/entry-server.ts">查看源码</a></td>
</tr>
</tbody>
</table>
<p>而原来的 <code>main.ts</code> 只作为入口函数导出，详见源码： <a href="https://github.com/chengpeiquan/vite-vue3-prerender-demo/blob/main/src/main.ts">main.ts</a></p>
<h2 id="路由"><a tabindex="-1" href="#路由"><span></span></a>路由</h2>
<p>不再需要手动配置路由结构了，改造后直接读取 <code>src/views</code> 的路由组件来生成页面路由。</p>
<p>详见源码： <a href="https://github.com/chengpeiquan/vite-vue3-prerender-demo/blob/main/src/router/index.ts">router</a></p>
<h2 id="seo-优化"><a tabindex="-1" href="#seo-优化"><span></span></a>SEO 优化</h2>
<p>做预渲染为的就是做 SEO ，所以需要自己提前配置好 SEO 的 TKD 三大要素，这里我也是放在 <code>src/router</code> 目录下一起管理了。</p>
<p>实现逻辑见 <a href="#%E9%A2%84%E6%B8%B2%E6%9F%93">预渲染</a> 部分的说明，这里是以最终每个页面的相对路径来判断要写入的 TKD 信息的。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#A626A4;--shiki-dark:#C586C0">export</span><span style="color:#E45649;--shiki-dark:#C586C0"> default</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  {</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    url</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '/'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    title</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '首页'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    description</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> '这是首页'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">,</span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#9CDCFE">    keywords</span><span style="color:#0184BC;--shiki-dark:#9CDCFE">:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> [</span><span style="color:#50A14F;--shiki-dark:#CE9178">'关键词1'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">, </span><span style="color:#50A14F;--shiki-dark:#CE9178">'关键词2'</span><span style="color:#383A42;--shiki-dark:#D4D4D4">],</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">  },</span></span>
<span class="line"><span style="color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#6A9955;--shiki-dark-font-style:inherit">  // ...</span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">]</span></span></code></pre>
<p>详见源码： <a href="https://github.com/chengpeiquan/vite-vue3-prerender-demo/blob/main/src/router/seo.ts">seo</a></p>
<h2 id="预渲染"><a tabindex="-1" href="#预渲染"><span></span></a>预渲染</h2>
<p><code>scripts/prerender.ts</code> 这个文件是执行预渲染行为，可以按照路由目录的结构渲染为静态 HTML 文件。</p>
<p>运行 <code>npm run generate</code> ，可以把 <code>dist/static</code> 作为静态站点部署。</p>
<p>当然我也封装了 <code>npm run build</code> 一次性编译所有平台（ Client / Server / Static ）。</p>
<p>详见： <a href="https://github.com/chengpeiquan/vite-vue3-prerender-demo/blob/main/package.json">package.json</a> 里的 <code>scripts</code> 部分。</p>
<h2 id="常见问题"><a tabindex="-1" href="#常见问题"><span></span></a>常见问题</h2>
<p>改造过程中遇到的几个问题：</p>
<h3 id="水合节点不匹配"><a tabindex="-1" href="#水合节点不匹配"><span></span></a>水合节点不匹配</h3>
<p>控制台报错：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Hydration</span><span style="color:#50A14F;--shiki-dark:#CE9178"> node</span><span style="color:#50A14F;--shiki-dark:#CE9178"> mismatch:</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">-</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Client</span><span style="color:#50A14F;--shiki-dark:#CE9178"> vnode:</span><span style="color:#50A14F;--shiki-dark:#CE9178"> div</span></span>
<span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">-</span><span style="color:#50A14F;--shiki-dark:#CE9178"> Server</span><span style="color:#50A14F;--shiki-dark:#CE9178"> rendered</span><span style="color:#50A14F;--shiki-dark:#CE9178"> DOM:</span><span style="color:#383A42;--shiki-dark:#D4D4D4"> &#x3C;</span><span style="color:#50A14F;--shiki-dark:#CE9178">!--app-html-</span><span style="color:#383A42;--shiki-dark:#D4D4D4">-></span></span></code></pre>
<p>警告来自于 <a href="https://github.com/vuejs/core/blob/main/packages/runtime-core/src/hydration.ts">hydration.ts</a> ，一般可以无视……</p>
<p>当然也可以了解更多的知识点： <a href="https://www.sumcumo.com/en/understand-and-solve-hydration-errors-in-vue-js">understand-and-solve-hydration-errors-in-vue-js</a></p>
<h3 id="路由跳转"><a tabindex="-1" href="#路由跳转"><span></span></a>路由跳转</h3>
<p>控制台报错：</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#4078F2;--shiki-dark:#DCDCAA">Unhandled</span><span style="color:#50A14F;--shiki-dark:#CE9178"> error</span><span style="color:#50A14F;--shiki-dark:#CE9178"> during</span><span style="color:#50A14F;--shiki-dark:#CE9178"> execution</span><span style="color:#50A14F;--shiki-dark:#CE9178"> of</span><span style="color:#50A14F;--shiki-dark:#CE9178"> scheduler</span><span style="color:#50A14F;--shiki-dark:#CE9178"> flush</span></span></code></pre>
<p>需要使用 <code>&#x3C;Suspense /></code> 标签来包裹路由视图，详见 <a href="https://v3.cn.vuejs.org/guide/migration/suspense.html#suspense">Suspense</a> 。</p>
<pre class="shiki shiki-themes one-light dark-plus" style="background-color:#FAFAFA;--shiki-dark-bg:#1E1E1E;color:#383A42;--shiki-dark:#D4D4D4" tabindex="0"><code class="language-diff"><span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">&#x3C;template></span></span>
<span class="line"><span style="color:#E45649;--shiki-dark:#CE9178">-  &#x3C;!-- &#x3C;router-view :key="key" /> --></span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+  &#x3C;router-view :key="key" v-slot="{ Component }"></span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+    &#x3C;Suspense></span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+      &#x3C;div></span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+        &#x3C;component :is="Component" /></span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+      &#x3C;/div></span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+    &#x3C;/Suspense></span></span>
<span class="line"><span style="color:#50A14F;--shiki-dark:#B5CEA8">+  &#x3C;/router-view></span></span>
<span class="line"><span style="color:#383A42;--shiki-dark:#D4D4D4">&#x3C;/template></span></span></code></pre>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
        <item>
            <title><![CDATA[Pinia怎么用？Vue3全局状态的管理工具Pinia教程]]></title>
            <link>https://chengpeiquan.com/article/pinia</link>
            <guid isPermaLink="true">https://chengpeiquan.com/article/pinia</guid>
            <pubDate>Mon, 04 Apr 2022 15:44:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Vue 官方推出的全局状态管理工具目前有 Vuex 和 Pinia ，两者的作用和用法都比较相似，但 Pinia 的设计更贴近 Vue 3 组合式 API 的用法。</p>
<h2 id="教程地址"><a tabindex="-1" href="#教程地址"><span></span></a>教程地址</h2>
<p>点击访问：<a href="https://vue3.chengpeiquan.com/pinia.html">全局状态的管理</a> 查看完整内容。</p>
<h2 id="为什么写这篇内容"><a tabindex="-1" href="#为什么写这篇内容"><span></span></a>为什么写这篇内容</h2>
<p>截止至 2022 年 4 月， Pinia 还没有被广泛的默认集成在各种脚手架里，官网也只有英文版，整理了教程便于提前学习。</p>
<p>由于 Vuex 4.x 版本只是个过渡版，Vuex 4 对 TypeScript 和 Composition API 都不是很友好，虽然官方团队在 GitHub 已有讨论 <a href="https://github.com/vuejs/rfcs/discussions/270">Vuex 5</a> 的开发提案，但从 2022-02-07 在 Vue 3 被设置为默认版本开始， Pinia 已正式被官方推荐作为全局状态管理的工具。</p>
<img src="https://cdn.chengpeiquan.com/img/2022/04/20220405012555.png?x-oss-process=image/interlace,1" alt="Pinia Logo">
<p>Pinia 支持 Vue 3 和 Vue 2 ，对 TypeScript 也有很完好的支持，延续了 <a href="https://vue3.chengpeiquan.com/">Vue3 入门指南与实战案例</a> 的宗旨，在这里只介绍基于 Vue 3 和 TypeScript 的用法。</p>
<h2 id="欢迎-star"><a tabindex="-1" href="#欢迎-star"><span></span></a>欢迎 Star</h2>
<p>教程属于 <a href="https://vue3.chengpeiquan.com/">Vue3 入门指南与实战案例</a> 的一部分，如果觉得对你有帮助，<a href="https://github.com/chengpeiquan/learning-vue3">欢迎到仓库给个 Star 鼓励</a>。</p>]]></content:encoded>
            <author>chengpeiquan@chengpeiquan.com (程沛权)</author>
        </item>
    </channel>
</rss>