<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://blog.biulight.top/timeline/blog</id>
    <title>Biulight 文档 Blog</title>
    <updated>2026-08-19T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://blog.biulight.top/timeline/blog"/>
    <subtitle>Biulight 文档 Blog</subtitle>
    <icon>https://blog.biulight.top/timeline/img/favicon.ico</icon>
    <rights>Copyright © 2026 Biulight</rights>
    <entry>
        <title type="html"><![CDATA[使用 Shine 快速配置 Ubuntu 服务器]]></title>
        <id>https://blog.biulight.top/timeline/blog/shine-ubuntu-server</id>
        <link href="https://blog.biulight.top/timeline/blog/shine-ubuntu-server"/>
        <updated>2026-08-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[拿到一台新的 Ubuntu 服务器后，可以用 Shine 统一完成常用命令行工具的安装和 shell profile 配置。相比逐条复制安装脚本，Shine 可以先预览将要执行的步骤，并记录已经初始化的项目，方便之后检查状态。]]></summary>
        <content type="html"><![CDATA[<p>拿到一台新的 Ubuntu 服务器后，可以用 Shine 统一完成常用命令行工具的安装和 shell profile 配置。相比逐条复制安装脚本，Shine 可以先预览将要执行的步骤，并记录已经初始化的项目，方便之后检查状态。</p>
<p>本文使用适合生产服务器的 <code>minimal</code> profile，安装 Neovim、fzf、bat、eza 和 zoxide，不包含 shell 历史同步、提示符、Node.js 工具链或 Homebrew。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="开始前">开始前<a href="https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E5%BC%80%E5%A7%8B%E5%89%8D" class="hash-link" aria-label="开始前的直接链接" title="开始前的直接链接" translate="no">​</a></h2>
<p>请先确认：</p>
<ul>
<li class="">当前系统为 Ubuntu；</li>
<li class="">服务器可以访问 GitHub 等软件下载源；</li>
<li class="">当前用户可以在需要时使用 <code>sudo</code>。</li>
</ul>
<p>系统初始化可能安装软件包、下载程序并更新 shell profile。正式执行前先使用 <code>--dry-run</code> 检查实际步骤。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="安装-shine">安装 Shine<a href="https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E5%AE%89%E8%A3%85-shine" class="hash-link" aria-label="安装 Shine的直接链接" title="安装 Shine的直接链接" translate="no">​</a></h2>
<p>运行官方安装脚本：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">curl -fsSL https://github.com/biulight/shine/releases/latest/download/install.sh | sh</span><br></span></code></pre></div></div>
<p>默认安装位置是 <code>~/.local/bin/shine</code>，安装脚本不会修改 shell 配置。确认 <code>~/.local/bin</code> 已加入 <code>PATH</code>，再检查版本：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">export PATH="$HOME/.local/bin:$PATH"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">shine --version</span><br></span></code></pre></div></div>
<p>需要设置永久 <code>PATH</code>、指定安装目录或固定版本时，请查看 Shine 的<a href="https://biulight.github.io/shine/zh-Hans/installation" target="_blank" rel="noopener noreferrer" class="">安装与升级</a>说明。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="预览服务器配置">预览服务器配置<a href="https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E9%A2%84%E8%A7%88%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%85%8D%E7%BD%AE" class="hash-link" aria-label="预览服务器配置的直接链接" title="预览服务器配置的直接链接" translate="no">​</a></h2>
<p>先查看当前版本为 Ubuntu 提供的初始化项目：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">shine sys list</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">shine sys bootstrap --preset minimal --dry-run</span><br></span></code></pre></div></div>
<p>第二条命令只打印选择结果、脚本调用和 profile 更新，不修改系统。确认项目和目标路径符合预期后，再继续执行。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="应用-minimal-profile">应用 minimal profile<a href="https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E5%BA%94%E7%94%A8-minimal-profile" class="hash-link" aria-label="应用 minimal profile的直接链接" title="应用 minimal profile的直接链接" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">shine sys bootstrap --preset minimal</span><br></span></code></pre></div></div>
<p>过程中可能因为安装系统软件而请求 <code>sudo</code> 权限。完成后查看 Shine 记录的初始化状态：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">shine sys status</span><br></span></code></pre></div></div>
<p>如果以后想检查这些软件是否有可用更新，可以运行：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">shine sys update</span><br></span></code></pre></div></div>
<p>该命令只检查由 <code>shine sys bootstrap</code> 记录的软件，不会自动安装升级。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="需要更完整的终端环境">需要更完整的终端环境<a href="https://blog.biulight.top/timeline/blog/shine-ubuntu-server#%E9%9C%80%E8%A6%81%E6%9B%B4%E5%AE%8C%E6%95%B4%E7%9A%84%E7%BB%88%E7%AB%AF%E7%8E%AF%E5%A2%83" class="hash-link" aria-label="需要更完整的终端环境的直接链接" title="需要更完整的终端环境的直接链接" translate="no">​</a></h2>
<p><code>minimal</code> 适合保持精简的生产服务器。如果这是开发机，或你还需要 AstroNvim、Atuin、Yazi、Starship 和 zsh-vi-mode，可以先预览 <code>recommended</code> profile：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">shine sys bootstrap --preset recommended --dry-run</span><br></span></code></pre></div></div>
<p>确认后去掉 <code>--dry-run</code> 即可应用。Ubuntu 还提供包含 ZeroTier、pnpm、mise 和 Homebrew 的 <code>all</code> profile；建议先对照<a href="https://biulight.github.io/shine/zh-Hans/reference/built-in-presets#%E7%B3%BB%E7%BB%9F%E9%A2%84%E8%AE%BE" target="_blank" rel="noopener noreferrer" class="">内置预设清单</a>，不要在服务器上安装不需要的组件。</p>
<p>更多交互选择、下载代理和受管系统配置的用法，见 Shine 中文手册的<a href="https://biulight.github.io/shine/zh-Hans/guides/system-init" target="_blank" rel="noopener noreferrer" class="">初始化与管理系统</a>。</p>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="Shine" term="Shine"/>
        <category label="Linux" term="Linux"/>
        <category label="Ubuntu" term="Ubuntu"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI 参与生产系统迁移：把模型能力放进可验证的交付流程]]></title>
        <id>https://blog.biulight.top/timeline/blog/ai-in-production-migration</id>
        <link href="https://blog.biulight.top/timeline/blog/ai-in-production-migration"/>
        <updated>2026-08-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[AI 可以很快读完一个仓库、列出迁移清单、批量修改代码；但在一个缺少业务和技术文档、仍在生产运行的]]></summary>
        <content type="html"><![CDATA[<p>AI 可以很快读完一个仓库、列出迁移清单、批量修改代码；但在一个缺少业务和技术文档、仍在生产运行的
系统里，“改得快”并不等于“可以上线”。这次 Vue 2 → Vue 3 迁移让我更确信：AI 最合适的位置不是
替代工程判断，而是进入一套有阶段、有证据、有回归边界的交付流程。</p>
<p>这篇文章不讨论 Vue API 本身，而是记录 AI 如何参与一个陌生生产系统的迁移：先收集事实、生成可审查
计划，再分批实施、手工验收，并把每次发现沉淀成下一轮可复用的规则。有关 Vue 迁移的技术细节见
<a class="" href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration">keystone-web 从 Vue 2 到 Vue 3：一场两段式迁移的复盘</a>。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="起点代码很多文档很少不能假装自己已经理解业务">起点：代码很多，文档很少，不能假装自己已经理解业务<a href="https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E8%B5%B7%E7%82%B9%E4%BB%A3%E7%A0%81%E5%BE%88%E5%A4%9A%E6%96%87%E6%A1%A3%E5%BE%88%E5%B0%91%E4%B8%8D%E8%83%BD%E5%81%87%E8%A3%85%E8%87%AA%E5%B7%B1%E5%B7%B2%E7%BB%8F%E7%90%86%E8%A7%A3%E4%B8%9A%E5%8A%A1" class="hash-link" aria-label="起点：代码很多，文档很少，不能假装自己已经理解业务的直接链接" title="起点：代码很多，文档很少，不能假装自己已经理解业务的直接链接" translate="no">​</a></h2>
<p>项目是一个中大型管理后台，有数百个 Vue 组件、权限路由、复杂表格、多个媒体类型的业务流程，以及
持续使用中的生产环境。接手时我并不熟悉业务，相关技术与业务文档也不完整。</p>
<p>在这种条件下，最危险的 AI 使用方式是把问题简化成一句“升级到 Vue 3”，然后接受一份看似完整的
改动。模型可以根据常见迁移模式给出合理建议，却不知道某个 <code>params</code> 是否承载业务身份、某个表格的
事件是否被下游依赖，或者一个管理员账号是否绕过了普通用户必经的权限路径。</p>
<p>因此先确定了一个原则：<strong>AI 不能以推断取代项目事实；每个结论都要有代码、测试、构建结果或手工观察
作为落点。</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="先降低未知数不要同时替换所有基础设施">先降低未知数：不要同时替换所有基础设施<a href="https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E5%85%88%E9%99%8D%E4%BD%8E%E6%9C%AA%E7%9F%A5%E6%95%B0%E4%B8%8D%E8%A6%81%E5%90%8C%E6%97%B6%E6%9B%BF%E6%8D%A2%E6%89%80%E6%9C%89%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD" class="hash-link" aria-label="先降低未知数：不要同时替换所有基础设施的直接链接" title="先降低未知数：不要同时替换所有基础设施的直接链接" translate="no">​</a></h2>
<p>迁移被拆成两段。第一段先将 Vue CLI 迁到 Rsbuild，但仍保持 Vue 2.7 上线。随后用约半年的稳定运行期
理解系统、清理无用代码与技术债，并在新改动中优先采用 Vue 2.7 和 Vue 3 共用的能力，例如
Composition API 和新插槽语法。</p>
<p>这一步不是“拖慢升级”，而是在为 AI 和人都创造更好的问题边界。等正式开始 Vue 3 迁移时，构建器、
环境变量、开发与生产构建已经经过验证；排查一个问题时，不必再同时怀疑 Rsbuild、Vue 运行时和业务
逻辑。先减少变量，之后的自动化才有可信的上下文。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="用-ai-写计划前先给它经过核实的输入">用 AI 写计划前，先给它经过核实的输入<a href="https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E7%94%A8-ai-%E5%86%99%E8%AE%A1%E5%88%92%E5%89%8D%E5%85%88%E7%BB%99%E5%AE%83%E7%BB%8F%E8%BF%87%E6%A0%B8%E5%AE%9E%E7%9A%84%E8%BE%93%E5%85%A5" class="hash-link" aria-label="用 AI 写计划前，先给它经过核实的输入的直接链接" title="用 AI 写计划前，先给它经过核实的输入的直接链接" translate="no">​</a></h2>
<p>计划阶段使用 Fable 5，但它不是从一句模糊需求开始猜。输入先由全仓检索和依赖排查组成，包括：</p>
<ul>
<li class="">331 个 <code>.vue</code> 组件的规模及业务模块分布；</li>
<li class="">151 处 <code>.sync</code>、122 个 <code>slot="..."</code> 命中、全局和局部指令的实际数量；</li>
<li class="">Vue Router、Vuex、Element UI、VXE Table、测试工具与构建配置的现状；</li>
<li class="">哪些高风险封装需要人工审查，哪些模式可以批量处理；</li>
<li class="">不做 UI 重设计、不换状态管理、不换构建器等范围边界。</li>
</ul>
<p>Fable 5 在这些事实之上生成迁移策略和 PRD：比较全量重写、长期 compat、微前端隔离三种路线；定义
基线、基础设施、第三方库、机械替换、公共组件、业务模块、测试升级和收尾验收等阶段；再为每一批
列出依赖关系和验证方式。</p>
<p>这里真正有价值的不是“AI 写出了一份很长的计划”，而是计划可以被审查、被纠正、被执行。比如
<code>slot="..."</code> 的 122 个命中并不等于 122 个模板迁移点：其中混有 JSX 中合法的属性和注释。把这个
前提写进 PRD，就能阻止后续执行模型做危险的全局替换。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="实施时把模型当作分批交付者而不是一次性重写器">实施时，把模型当作分批交付者，而不是一次性重写器<a href="https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E5%AE%9E%E6%96%BD%E6%97%B6%E6%8A%8A%E6%A8%A1%E5%9E%8B%E5%BD%93%E4%BD%9C%E5%88%86%E6%89%B9%E4%BA%A4%E4%BB%98%E8%80%85%E8%80%8C%E4%B8%8D%E6%98%AF%E4%B8%80%E6%AC%A1%E6%80%A7%E9%87%8D%E5%86%99%E5%99%A8" class="hash-link" aria-label="实施时，把模型当作分批交付者，而不是一次性重写器的直接链接" title="实施时，把模型当作分批交付者，而不是一次性重写器的直接链接" translate="no">​</a></h2>
<p>Opus 4.7 与 Sonnet 5 按 PRD 分批实施。每个批次都有清楚的范围和退出条件：例如先让基础设施能进入
登录页，再替换 Element UI 和 VXE Table，并用真实列表页验证；之后才处理 <code>.sync</code>、插槽、指令等
全仓变化；最后按公共组件、业务模块的依赖顺序推进。</p>
<p>这套节奏给 AI 加了三层约束：</p>
<ol>
<li class=""><strong>改动范围小。</strong> 每一批聚焦一类依赖或一个模块，失败时能回到明确的 diff。</li>
<li class=""><strong>验证紧随改动。</strong> 构建、lint、单测、E2E 和手工页面验证按阶段执行，不等到最后才发现问题。</li>
<li class=""><strong>不夹带“顺手优化”。</strong> 迁移期间发现的无关重构只记录，不在同一批改动中扩张范围。</li>
</ol>
<p>模型很擅长重复性工作：全仓盘点、模式转换、补齐大量相似调用点、汇总失败清单。它不擅长在缺少上下文
时猜测某个行为是否是业务约定。因此，批次拆分的意义不是限制 AI 的速度，而是让每次推断都有机会被
最小范围的验证推翻。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="真实反例一个看似合理的路由规则如何造成回归">真实反例：一个看似合理的路由规则如何造成回归<a href="https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E7%9C%9F%E5%AE%9E%E5%8F%8D%E4%BE%8B%E4%B8%80%E4%B8%AA%E7%9C%8B%E4%BC%BC%E5%90%88%E7%90%86%E7%9A%84%E8%B7%AF%E7%94%B1%E8%A7%84%E5%88%99%E5%A6%82%E4%BD%95%E9%80%A0%E6%88%90%E5%9B%9E%E5%BD%92" class="hash-link" aria-label="真实反例：一个看似合理的路由规则如何造成回归的直接链接" title="真实反例：一个看似合理的路由规则如何造成回归的直接链接" translate="no">​</a></h2>
<p>最有教育意义的不是成功案例，而是一次错误的启发式。</p>
<p>迁移 Vue Router 3 到 4 时，曾把“内部 <code>router.push()</code> 应使用 <code>query</code>，构造可分享链接的
<code>router.resolve()</code> 应使用 <code>params</code>”当作批量修改规则。这个规则很顺口，但它是错的。</p>
<p>真正决定参数位置的是目标路由的 <code>path</code>：未声明动态段的值使用 <code>query</code>；必填动态段必须使用
<code>params</code>；可选动态段再按约定选择。错误规则把某个内部跳转的必填 <code>id</code> 放进 <code>query</code>，结果在点击列表
行时同步抛出 <code>Missing required param</code>，跳转被直接阻断。</p>
<p>这次错误没有被隐藏。它被修复后，完整的根因、适用规则和反例被写进迁移记录，并进一步沉淀为项目
记忆。下一轮 Claude Code 会话不需要“记得上一次聊天”，只需要读取这条规则。</p>
<p>我认为这正是 AI 编程在生产环境里最重要的能力之一：<strong>把失败从一次对话中的偶然经验，变成仓库中可
检查、可复用、可更新的知识。</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="测试覆盖不是安全感手工验收负责发现模型和用例的共同盲区">测试覆盖不是安全感：手工验收负责发现模型和用例的共同盲区<a href="https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E6%B5%8B%E8%AF%95%E8%A6%86%E7%9B%96%E4%B8%8D%E6%98%AF%E5%AE%89%E5%85%A8%E6%84%9F%E6%89%8B%E5%B7%A5%E9%AA%8C%E6%94%B6%E8%B4%9F%E8%B4%A3%E5%8F%91%E7%8E%B0%E6%A8%A1%E5%9E%8B%E5%92%8C%E7%94%A8%E4%BE%8B%E7%9A%84%E5%85%B1%E5%90%8C%E7%9B%B2%E5%8C%BA" class="hash-link" aria-label="测试覆盖不是安全感：手工验收负责发现模型和用例的共同盲区的直接链接" title="测试覆盖不是安全感：手工验收负责发现模型和用例的共同盲区的直接链接" translate="no">​</a></h2>
<p>自动化用例能保证核心路径持续可跑，却不天然保证覆盖正确。一次手工回归发现，已有 Playwright 冒烟
只用管理员账号登录；管理员恰好绕开了普通角色才会走到的 Campaign 详情重定向，因此上述路由回归
没有被 E2E 捕获。</p>
<p>手工验收还发现过很多“构建、lint、单测都正常”的问题：动态组件在页面显示 <code>[object Promise]</code>、
Element Plus 日期控件因格式 token 或裸 <code>default-time</code> 解析失败而显示错误日期、全局 passive 事件
补丁让下拉组件的鼠标选择失效。这些问题往往需要真实数据、空状态、特定角色或连续交互才能出现。</p>
<p>所以手工验收不是自动化测试的低配替代，而是专门覆盖高不确定性区域的另一层测试。对管理后台而言，
至少应显式覆盖：</p>
<ul>
<li class="">管理员与非管理员的菜单、深链接和权限拦截；</li>
<li class="">有数据、空数据、可清空字段和默认值；</li>
<li class="">表格分页、筛选、滚动、弹窗、Popover、上传和动态组件；</li>
<li class="">控制台中的框架、路由和组件库警告。</li>
</ul>
<p>每个手工发现都应有去向：补成自动化用例、加入手工清单，或提炼为迁移规则。没有去向的“已修复”只是
下一次回归的候选。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="ai-需要的不只是上下文窗口而是项目记忆系统">AI 需要的不只是上下文窗口，而是项目记忆系统<a href="https://blog.biulight.top/timeline/blog/ai-in-production-migration#ai-%E9%9C%80%E8%A6%81%E7%9A%84%E4%B8%8D%E5%8F%AA%E6%98%AF%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AA%97%E5%8F%A3%E8%80%8C%E6%98%AF%E9%A1%B9%E7%9B%AE%E8%AE%B0%E5%BF%86%E7%B3%BB%E7%BB%9F" class="hash-link" aria-label="AI 需要的不只是上下文窗口，而是项目记忆系统的直接链接" title="AI 需要的不只是上下文窗口，而是项目记忆系统的直接链接" translate="no">​</a></h2>
<p>这次实践中，迁移信息被拆成不同层次保存：</p>
<table><thead><tr><th>载体</th><th>负责回答的问题</th></tr></thead><tbody><tr><td>迁移 PRD</td><td>为什么采用这条路线、阶段如何依赖、每批如何验收</td></tr><tr><td>迁移问题记录</td><td>具体故障的现象、根因、修复方式与回归规则</td></tr><tr><td>项目 AGENTS.md</td><td>当前技术栈、开发约束、验证命令和禁止重新引入的兼容层</td></tr><tr><td>项目记忆</td><td>一条会在后续任务反复使用的精确规则，例如路由参数边界</td></tr><tr><td>Git 提交与测试</td><td>改动的可审查证据、可回退边界与行为验证</td></tr></tbody></table>
<p>这种分层比把所有信息塞进一个超长提示词更可靠。提示词会过期、上下文会被截断、不同模型的理解也会
不同；而仓库里的文档、规则和测试可以随实现变化一起维护。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="我会继续坚持的边界">我会继续坚持的边界<a href="https://blog.biulight.top/timeline/blog/ai-in-production-migration#%E6%88%91%E4%BC%9A%E7%BB%A7%E7%BB%AD%E5%9D%9A%E6%8C%81%E7%9A%84%E8%BE%B9%E7%95%8C" class="hash-link" aria-label="我会继续坚持的边界的直接链接" title="我会继续坚持的边界的直接链接" translate="no">​</a></h2>
<p>AI 参与生产改造时，我会把下面几条当作默认约束：</p>
<ol>
<li class="">让 AI 先完成全仓盘点和证据收集，再由人工审查盘点结论、范围与风险后制定方案。</li>
<li class="">将大迁移拆成可独立验证、可回退的小批次。</li>
<li class="">对 API 行为、权限、路由和组件库差异，优先相信源码、测试和真实页面，而不是“通常如此”。</li>
<li class="">把模型造成或发现的错误写成规则和测试，而不是只留在聊天记录里。</li>
<li class="">保留人工验收，特别是角色、空状态、视觉和连续交互路径。</li>
<li class="">不把迁移、性能优化和业务重构混在同一轮交付中。</li>
</ol>
<p>AI 没有让生产迁移变成零风险工作；它减少的是盘点、重复修改和信息整理的成本。真正让风险可控的，
仍然是清晰的范围、持续的验证、可回退的提交，以及愿意把错误沉淀下来的人。</p>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="AI" term="AI"/>
        <category label="Claude Code" term="Claude Code"/>
        <category label="Fable" term="Fable"/>
        <category label="Opus" term="Opus"/>
        <category label="Sonnet" term="Sonnet"/>
        <category label="Software Engineering" term="Software Engineering"/>
        <category label="Migration" term="Migration"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[keystone-web 从 Vue 2 到 Vue 3：一场两段式迁移的复盘]]></title>
        <id>https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration</id>
        <link href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration"/>
        <updated>2026-08-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[把一个有 331 个 .vue 组件、仍在持续迭代的管理后台从 Vue 2 迁到 Vue 3，最危险的想法是：]]></summary>
        <content type="html"><![CDATA[<p>把一个有 331 个 <code>.vue</code> 组件、仍在持续迭代的管理后台从 Vue 2 迁到 Vue 3，最危险的想法是：
“把依赖升级完，再把报错逐个修掉。”真正的迁移对象并不只有 Vue，而是路由、状态管理、UI 库、
表格封装、构建链、测试和数百个已经被用户使用的交互。</p>
<p>这次升级实际上分成了两段：先将 Vue 2.x + Vue CLI 迁到 Vue 2.7 + Rsbuild 并上线；随后让它在线上
稳定运行约半年，在这段时间持续清理技术债、删除不再需要的代码、尽量精简实现；最后才将已经稳定的
Vue 2.7 应用升级到 Vue 3。第二段在专用分支中阶段化完成，旧版本继续承接发布，迁移版本验收后再
整体切换。它没有演变成业务重写，也没有趁机换 Pinia 或 Vite；这份克制比任何 API 转换技巧都更重要。</p>
<table><thead><tr><th>阶段</th><th>主要工作</th><th>结束条件</th></tr></thead><tbody><tr><td>第一段：构建迁移</td><td>Vue CLI → Vue 2.7 + Rsbuild，保持业务行为</td><td>新构建链上线并稳定运行</td></tr><tr><td>稳定与清债期</td><td>熟悉业务边界，删除无用代码，使用跨 Vue 2.7 / Vue 3 的 API</td><td>业务边界逐步明确，待迁移代码范围收敛</td></tr><tr><td>第二段：运行时迁移</td><td>Vue 3、Router 4、Element Plus、VXE Table 4 与测试体系升级</td><td>移除兼容层，完成自动化与手工验收</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="第一段先把构建系统换掉再给业务留出稳定期">第一段：先把构建系统换掉，再给业务留出稳定期<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E7%AC%AC%E4%B8%80%E6%AE%B5%E5%85%88%E6%8A%8A%E6%9E%84%E5%BB%BA%E7%B3%BB%E7%BB%9F%E6%8D%A2%E6%8E%89%E5%86%8D%E7%BB%99%E4%B8%9A%E5%8A%A1%E7%95%99%E5%87%BA%E7%A8%B3%E5%AE%9A%E6%9C%9F" class="hash-link" aria-label="第一段：先把构建系统换掉，再给业务留出稳定期的直接链接" title="第一段：先把构建系统换掉，再给业务留出稳定期的直接链接" translate="no">​</a></h2>
<p>第一段从 Vue CLI 5 迁到 Rsbuild 时，应用仍运行在 Vue 2.7。目标不是借机升级框架，而是将构建链
现代化并消除已经没有价值的历史配置：移除 Vue CLI 及其插件、删除 <code>vue.config.js</code> 等旧配置，改用
<code>rsbuild.config.mjs</code>；SVG 从不兼容 Rspack 的 <code>svg-sprite-loader</code> 改为原生 <code>asset/source</code> 加载，
组件的使用方式保持不变。</p>
<p>采用这条路线也有现实原因：接手时我对这个项目并不熟悉，手上缺少完整的业务与技术文档。此时若同时
替换构建器、运行时、组件库并修改业务逻辑，任何异常都很难回答“它来自旧行为、构建链、Vue 3，还是
我尚未理解的业务规则”。先把范围收缩为 Vue 2.7 + Rsbuild 上线，才能在不改变主要业务语义的前提下
建立对系统的认识。</p>
<p>这次迁移也统一了浏览器端环境变量边界。业务代码从 <code>process.env.VUE_APP_*</code> 改为
<code>import.meta.env.VUE_APP_*</code>；少数第三方包所需的 <code>process.env</code> 则只在构建配置中提供兼容 shim。
这样既避免浏览器里的 <code>process is not defined</code>，也不需要为了兼容旧写法继续在业务代码中传播
Node.js 运行时假设。</p>
<p>Rsbuild 上线后，项目在 Vue 2.7 状态继续正常迭代约半年。这段稳定期的重点不是继续验证已经上线的
构建工具，而是让团队通过日常需求和真实业务流程，逐步确认业务边界与历史行为。在这个过程中，团队
持续清理技术债、删除非必要代码，让后续需要维护和迁移的范围更小、更清晰。等到 Vue 3 迁移启动时，
面对的是一套业务基线清楚、代码范围已经收敛的系统，不再需要一边迁移一边猜测旧逻辑。</p>
<p>这也是两段式路线最大的收益——先完成构建工具切换，再留出理解业务和收敛代码的稳定阶段，避免把
构建系统变化、Vue 运行时升级和组件库替换压在同一批改动中。第二阶段因此可以把注意力集中在 Vue 3
迁移本身。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="vue-27-不是终点而是-vue-3-的预备跑道">Vue 2.7 不是终点，而是 Vue 3 的预备跑道<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#vue-27-%E4%B8%8D%E6%98%AF%E7%BB%88%E7%82%B9%E8%80%8C%E6%98%AF-vue-3-%E7%9A%84%E9%A2%84%E5%A4%87%E8%B7%91%E9%81%93" class="hash-link" aria-label="Vue 2.7 不是终点，而是 Vue 3 的预备跑道的直接链接" title="Vue 2.7 不是终点，而是 Vue 3 的预备跑道的直接链接" translate="no">​</a></h2>
<p>这半年的重构并非只做删除。Vue 2.7 原生提供了 Composition API，因此在新增或调整逻辑时，优先选用
<code>ref</code>、<code>reactive</code>、<code>computed</code>、<code>watch</code>、<code>onMounted</code> 等 Vue 2.7 与 Vue 3 都支持的能力；保留既有
Options API，但不再继续扩散只服务于 Vue 2 的写法。这样做不会改变已经稳定的页面风格，却能让每一次
局部维护都向 Vue 3 靠近。</p>
<p><code>this.$set</code> 是典型例子。Vue 2 需要它来让新增对象属性或数组索引更新具备响应式；Vue 3 基于 Proxy，
普通赋值已经会响应。预备期内，改到相关代码时就倾向写成 <code>object[key] = value</code> 或
<code>array[index] = value</code>，而不是继续引入 <code>$set</code>。这既减少了后续迁移的工作量，也让代码表达的意图更
直接。</p>
<p>ESLint 在这个过程中承担的是“逐步收紧的迁移护栏”，而不是一次性把历史代码全部打红。Vue 2.7 阶段，
为避免存量项目立刻被遗留写法阻断，曾对 <code>v-bind.sync</code>、<code>$set</code> / <code>$delete</code> 相关的废弃规则保留豁免；
同时约束新增或重构代码不要继续引入这些 Vue 2 专属模式。正式切到 Vue 3 后，配置目标才改为 Vue 3.5
并移除豁免，让 lint 明确报出 <code>.sync</code>、旧销毁生命周期、<code>.native</code>、<code>$listeners</code> 等不再可用的写法，
而不是让它们在运行时才暴露。</p>
<p>这条护栏最终找出了 18 个文件中的 48 处 <code>$set</code> 调用，并将它们全部替换为普通对象或数组赋值；项目里
没有实际的 <code>$delete</code> 调用。14 个文件中的 20 处 <code>.native</code> 以及 <code>destroyed</code> / <code>beforeDestroy</code> 生命周期，
则属于<strong>切换到 Vue 3 运行时之后</strong>才能处理的破坏性变更：它们分别改为 Vue 3 的事件透传方式和
<code>unmounted</code> / <code>beforeUnmount</code>。后者不能提前放进 Vue 2.7 代码库。重要的不是这些数字本身，而是节奏：
先让新的维护代码不再制造 Vue 2 专属债务，再在运行时切换后用规则集中清理只能由 Vue 3 支持的写法。</p>
<p>插槽也遵循同一原则。Vue 2.7 已支持 Vue 3 使用的 <code>v-slot</code> / <code>#</code> 语法，因此在持续重构中，模板插槽
应从旧的 <code>slot="name"</code>、<code>slot-scope="scope"</code> 迁向 <code>#name="scope"</code>。例如：</p>
<div class="language-vue codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-vue codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">&lt;!-- Vue 2 旧写法：Vue 3 不支持 --&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">&lt;template slot="default" slot-scope="scope"&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  {{ scope.row.name }}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">&lt;/template&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">&lt;!-- Vue 2.7 与 Vue 3 都支持 --&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">&lt;template #default="scope"&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  {{ scope.row.name }}</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">&lt;/template&gt;</span><br></span></code></pre></div></div>
<p>不过这不是一次安全的全文替换。全仓检索到 122 个 <code>slot="..."</code> 命中，其中混有真正需要迁移的模板、
JSX render 中合法的 <code>slot</code> 属性，以及注释里的死代码。正确做法是先让新增和改动的模板采用
<code>v-slot</code>，再在 Vue 3 阶段逐个分类处理存量命中；把 JSX 一并替换反而会制造新的问题。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="ai-的角色先生成可审查的计划再分批执行">AI 的角色：先生成可审查的计划，再分批执行<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#ai-%E7%9A%84%E8%A7%92%E8%89%B2%E5%85%88%E7%94%9F%E6%88%90%E5%8F%AF%E5%AE%A1%E6%9F%A5%E7%9A%84%E8%AE%A1%E5%88%92%E5%86%8D%E5%88%86%E6%89%B9%E6%89%A7%E8%A1%8C" class="hash-link" aria-label="AI 的角色：先生成可审查的计划，再分批执行的直接链接" title="AI 的角色：先生成可审查的计划，再分批执行的直接链接" translate="no">​</a></h2>
<p>全仓盘点完成后，Fable 5 基于已核实的依赖、特殊用法和范围边界生成迁移 PRD；Opus 4.7 与 Sonnet 5
再按阶段、任务批次和验证门槛实施。Claude Code 则把手工验收中发现的现象、根因和规则持续沉淀到
迁移记录与项目记忆中。AI 加速了盘点、模式化修改与信息整理，但每一个结论仍须由代码、测试、构建
结果或手工观察验证。</p>
<p>这套协作方式、一次路由参数误判造成的回归，以及自动化和人工验收如何互补，详见
<a class="" href="https://blog.biulight.top/timeline/blog/ai-in-production-migration">AI 参与生产系统迁移：把模型能力放进可验证的交付流程</a>。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="先做选择为什么不是长期渐进迁移">先做选择：为什么不是长期渐进迁移<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%85%88%E5%81%9A%E9%80%89%E6%8B%A9%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E6%98%AF%E9%95%BF%E6%9C%9F%E6%B8%90%E8%BF%9B%E8%BF%81%E7%A7%BB" class="hash-link" aria-label="先做选择：为什么不是长期渐进迁移的直接链接" title="先做选择：为什么不是长期渐进迁移的直接链接" translate="no">​</a></h2>
<p>进入第二段时，项目已经是 Vue 2.7、Vue Router 3、Vuex 3、Element UI 2、VXE Table 3、
vue-i18n 8 和 Rsbuild 的组合。目标是 Vue 3.5、Vue Router 4、Vuex 4、Element Plus、VXE Table 4
和 vue-i18n 9，同时保留已经稳定的 Rsbuild 与既有 Vuex 模块结构。</p>
<p>一开始看起来有三条路：全量重写、长期使用 <code>@vue/compat</code> 渐进迁移，或用微前端让两套运行时并行。
最后都不适合这个项目：</p>
<ul>
<li class="">331 个组件的全量重写会把业务回归风险推到最高；</li>
<li class="">在本项目中，Element UI 2 与 VXE Table 3 不具备可作为发布形态的 Vue 3/compat 支持，两项核心 UI/表格依赖仍须整体迁至对应的 Vue 3 版本；</li>
<li class="">管理后台共享登录态、布局、权限与路由，专门建设微前端的成本远高于收益。</li>
</ul>
<p>于是选择“阶段化的一次性迁移”。<code>@vue/compat</code> 只在中间阶段以 MODE 2 运行，用运行时警告当作
迁移待办扫描器；发布前必须移除。这个边界很关键：兼容层可以帮助发现问题，但不能成为问题被搁置的
理由。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="先固定什么都不改时的基线">先固定什么都不改时的基线<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%85%88%E5%9B%BA%E5%AE%9A%E4%BB%80%E4%B9%88%E9%83%BD%E4%B8%8D%E6%94%B9%E6%97%B6%E7%9A%84%E5%9F%BA%E7%BA%BF" class="hash-link" aria-label="先固定什么都不改时的基线的直接链接" title="先固定什么都不改时的基线的直接链接" translate="no">​</a></h2>
<p>迁移第一步不是改 <code>main.js</code>，而是建立可回归的事实基线。</p>
<p>项目先补齐并跑通 Playwright 冒烟链路：登录、Dashboard、Campaign 列表和分页、报表、组织管理
弹窗。它们覆盖了权限路由、VXE Table、Element UI 表单和常见异步请求。与此同时，用
<code>pnpm analyze</code> 记录生产包体：迁移前未压缩总大小为 15345.0 kB，gzip 后为 7397.6 kB。</p>
<p>这份基线让后续每一个“看起来像 Vue 3 问题”的现象都有判断依据：它是原有问题、迁移回归，还是
尚未覆盖的新路径。没有它，构建通过只会给人错误的安全感。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="分阶段切换先打通运行时再迁业务">分阶段切换：先打通运行时，再迁业务<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%88%86%E9%98%B6%E6%AE%B5%E5%88%87%E6%8D%A2%E5%85%88%E6%89%93%E9%80%9A%E8%BF%90%E8%A1%8C%E6%97%B6%E5%86%8D%E8%BF%81%E4%B8%9A%E5%8A%A1" class="hash-link" aria-label="分阶段切换：先打通运行时，再迁业务的直接链接" title="分阶段切换：先打通运行时，再迁业务的直接链接" translate="no">​</a></h2>
<p>迁移没有按目录从上到下改，而是按依赖关系推进。</p>
<p>第一阶段替换框架基础设施：Rsbuild 的 Vue 2 插件换成 Vue 3 插件，<code>new Vue()</code> 改为
<code>createApp()</code>，<code>Vue.prototype</code> 改到 <code>app.config.globalProperties</code>，路由切到
<code>createRouter()</code> / <code>createWebHistory()</code>，并将动态路由注册从 <code>addRoutes</code> 改为逐条 <code>addRoute</code>。
Vuex 3 升 Vuex 4 后，已有五个 namespaced 模块的业务 API 基本不需要改；vue-i18n 9 则采用
legacy 模式，降低了模板国际化调用的改造量。</p>
<p>第二阶段才整体替换第三方库：Element UI → Element Plus，VXE Table 3 → 4，并接入与 Element
Plus 对应的 VXE 渲染插件。紧接着处理 Vue 3 的机械性变化，例如 <code>.sync</code> 改 <code>v-model:prop</code>、
指令钩子改名、过时插槽语法、<code>$listeners</code>、<code>$scopedSlots</code> 和 test-utils v2 的配置变化。</p>
<p>机械替换也有边界。全仓有 151 处 <code>.sync</code>，可以分批转换并结合 diff 与构建检查；但 <code>slot="..."</code>
命中了 122 个文件，其中既有待迁的模板语法，也有合法的 JSX 属性和注释。对它全局替换会直接破坏
代码。类似地，路由守卫、全局指令、BiuTable / selectgrid 这类 VXE Table 深度封装，以及
BiuDialog / BiuForm 这样的基础组件，都必须按实际行为人工审查。</p>
<p>业务页面放到最后，并按依赖与规模推进：先公共页、登录、Dashboard，再到系统配置、报表、订单，
最后才是近百个组件的 Campaign 模块。每批变更独立提交，并在进入下一批前运行构建、单测和对应
页面的手工验证。这样出现回归时，排查范围仍然可控。</p>
<p>每个阶段采用的验证目标也不同：基础设施阶段先验证应用能到登录页；替换组件库后验证 Dashboard
和一个 VXE Table 列表；公共组件阶段为每一批组件找一个宿主页面；业务模块阶段再执行相应 E2E 与
完整手工清单。最后才同时要求 lint、单测、生产构建、E2E、兼容警告清理和包体对比。把验证放在
阶段门槛上，比在最后堆一次全量测试更容易定位问题。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="验收阶段发现最棘手的错误往往不在构建期">验收阶段发现：最棘手的错误往往不在构建期<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E9%AA%8C%E6%94%B6%E9%98%B6%E6%AE%B5%E5%8F%91%E7%8E%B0%E6%9C%80%E6%A3%98%E6%89%8B%E7%9A%84%E9%94%99%E8%AF%AF%E5%BE%80%E5%BE%80%E4%B8%8D%E5%9C%A8%E6%9E%84%E5%BB%BA%E6%9C%9F" class="hash-link" aria-label="验收阶段发现：最棘手的错误往往不在构建期的直接链接" title="验收阶段发现：最棘手的错误往往不在构建期的直接链接" translate="no">​</a></h2>
<p>下面几个问题都没有被“构建成功”捕获，却最能说明为什么迁移需要真实页面和真实交互的回归。</p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="动态组件渲染出-object-promise">动态组件渲染出 <code>[object Promise]</code><a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6%E6%B8%B2%E6%9F%93%E5%87%BA-object-promise" class="hash-link" aria-label="动态组件渲染出-object-promise的直接链接" title="动态组件渲染出-object-promise的直接链接" translate="no">​</a></h3>
<p>Vue 2 会把 <code>:is</code> 接收的 <code>() =&gt; import('./Component.vue')</code> 当作异步组件工厂；Vue 3 不再隐式做
这个判断。Campaign 详情页按媒体类型切换子组件时，页面没有报错，只是渲染出了
<code>[object Promise]</code>。</p>
<p>修复是显式使用 <code>defineAsyncComponent(() =&gt; import('./Component.vue'))</code>。这是一个很好的提醒：
静态检查能发现类型和语法问题，但无法替代“实际点开动态分支”的验证。</p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="vue-router-4-中-params-与-query-不能凭习惯替换">Vue Router 4 中 <code>params</code> 与 <code>query</code> 不能凭习惯替换<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#vue-router-4-%E4%B8%AD-params-%E4%B8%8E-query-%E4%B8%8D%E8%83%BD%E5%87%AD%E4%B9%A0%E6%83%AF%E6%9B%BF%E6%8D%A2" class="hash-link" aria-label="vue-router-4-中-params-与-query-不能凭习惯替换的直接链接" title="vue-router-4-中-params-与-query-不能凭习惯替换的直接链接" translate="no">​</a></h3>
<p>迁移时若向没有动态路径段的路由传 <code>params</code>，Vue Router 4 会丢弃它并给出警告；而把本应填入必填
动态段的值改放到 <code>query</code>，又会直接抛出 <code>Missing required param</code>，使跳转失败。</p>
<p>最终采用的规则不是“<code>push</code> 用 query、<code>resolve</code> 用 params”，而是只看目标路由的 <code>path</code>：</p>
<ul>
<li class="">没有声明动态段的值，一律用 <code>query</code>；</li>
<li class="">必填动态段必须用 <code>params</code>；</li>
<li class="">可选动态段两者都可用，但要保持同一路由的读取约定一致。</li>
</ul>
<p>这条规则后来也指导了可分享 Campaign 链接和内部跳转的收敛，避免了“页面能打开但详情参数丢失”的
静默回归。</p>
<p>这里还有一个值得记录的过程教训：最初的错误启发式是“内部 <code>push()</code> 用 query，构造可分享链接的
<code>resolve()</code> 用 params”。它听起来合理，却忽略了路由定义本身；一次批量替换后，普通列表点击也因
缺少必填 <code>id</code> 而被阻断。修复后，这条规则被单独写入项目记忆。对于 AI 参与的大范围迁移，记录
“曾经为什么改错”与记录最终写法同样重要。</p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="element-plus-的日期问题会静默污染业务值">Element Plus 的日期问题会静默污染业务值<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#element-plus-%E7%9A%84%E6%97%A5%E6%9C%9F%E9%97%AE%E9%A2%98%E4%BC%9A%E9%9D%99%E9%BB%98%E6%B1%A1%E6%9F%93%E4%B8%9A%E5%8A%A1%E5%80%BC" class="hash-link" aria-label="Element Plus 的日期问题会静默污染业务值的直接链接" title="Element Plus 的日期问题会静默污染业务值的直接链接" translate="no">​</a></h3>
<p>Element UI 的日期格式使用 <code>yyyy</code>、<code>dd</code> 等小写 token；Element Plus 基于 dayjs，正确写法是
<code>YYYY</code>、<code>DD</code>。错误 token 不只让输入框显示 <code>Su/02/yyyy</code> 这类乱码，打开面板后会发现实际选中日期
已经错位。</p>
<p>另一个更隐蔽的问题是 <code>default-time="['00:00:00', '23:59:59']"</code>。在 Element Plus 中，裸时间
字符串会被解析为 Invalid Date；当日期范围控件没有绑定值时，内部日期计算继而变成 <code>NaN</code>，日历
面板整体失效。解决方式是使用完整且合法的日期时间字符串，例如
<code>'2000-01-01 00:00:00'</code>。这类差异无法靠 API 名称替换发现，必须在“编辑已有数据”和“新建空数据”
两种场景中分别打开控件核验。</p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="全局性能补丁会破坏组件交互">全局性能补丁会破坏组件交互<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E5%85%A8%E5%B1%80%E6%80%A7%E8%83%BD%E8%A1%A5%E4%B8%81%E4%BC%9A%E7%A0%B4%E5%9D%8F%E7%BB%84%E4%BB%B6%E4%BA%A4%E4%BA%92" class="hash-link" aria-label="全局性能补丁会破坏组件交互的直接链接" title="全局性能补丁会破坏组件交互的直接链接" translate="no">​</a></h3>
<p>Campaign 表单中的 <code>vue-multiselect</code> 出现了鼠标点选即关闭、值无法回填的问题，程序化
<code>element.click()</code> 却正常。根因不是组件库或弹窗焦点，而是旧项目引入的
<code>default-passive-events</code>：它把 <code>mousedown</code> 也强制设为 passive，导致组件内部用于保持焦点的
<code>preventDefault()</code> 失效。</p>
<p>修复不是给每个下拉框补事件，而是替换全局补丁，只将 <code>scroll</code>、<code>wheel</code>、<code>touch*</code> 等真正可能阻塞
滚动的事件设为 passive，保留鼠标按键事件的默认行为。迁移过程中遇到“许多地方同时出问题”时，
先检查全局拦截器、样式覆盖和事件补丁，往往比逐页修补更有效。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="自动化回归不等于验收角色和空状态必须进入清单">自动化回归不等于验收：角色和空状态必须进入清单<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E8%87%AA%E5%8A%A8%E5%8C%96%E5%9B%9E%E5%BD%92%E4%B8%8D%E7%AD%89%E4%BA%8E%E9%AA%8C%E6%94%B6%E8%A7%92%E8%89%B2%E5%92%8C%E7%A9%BA%E7%8A%B6%E6%80%81%E5%BF%85%E9%A1%BB%E8%BF%9B%E5%85%A5%E6%B8%85%E5%8D%95" class="hash-link" aria-label="自动化回归不等于验收：角色和空状态必须进入清单的直接链接" title="自动化回归不等于验收：角色和空状态必须进入清单的直接链接" translate="no">​</a></h2>
<p>迁移中的 Playwright 冒烟用例覆盖了登录、Dashboard 和核心列表，但一次后续排查发现，它只使用
管理员账号。管理员的权限路由恰好绕过了非管理员才会经过的 Campaign 详情重定向，因此路由参数的
问题没有在 E2E 中出现，而是在手工以普通角色进入详情时才暴露。</p>
<p>这不是“自动化测试没价值”，而是覆盖模型不完整。对于权限驱动的管理后台，回归清单至少还应包含：</p>
<ul>
<li class="">一种非管理员角色的登录、菜单生成和深链接访问；</li>
<li class="">有数据与无数据两种表单状态，尤其是日期、默认值和清空操作；</li>
<li class="">列表替换、分页或媒体类型切换后的滚动位置与筛选条件；</li>
<li class="">弹窗、Popover、上传和动态组件等只在后续交互出现的分支；</li>
<li class="">浏览器控制台中的 Vue、路由和组件库警告。</li>
</ul>
<p>手工 QA 的价值不是重复点一遍自动化脚本，而是专门覆盖权限、真实数据形态和视觉交互这些很难在
一次通用冒烟中穷尽的变量。每个发现都应回流为新的自动化用例、手工检查项，或至少是一条有边界的
迁移规则。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="构建产物不能只看是否超过预算">构建产物：不能只看是否超过预算<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E6%9E%84%E5%BB%BA%E4%BA%A7%E7%89%A9%E4%B8%8D%E8%83%BD%E5%8F%AA%E7%9C%8B%E6%98%AF%E5%90%A6%E8%B6%85%E8%BF%87%E9%A2%84%E7%AE%97" class="hash-link" aria-label="构建产物：不能只看是否超过预算的直接链接" title="构建产物：不能只看是否超过预算的直接链接" translate="no">​</a></h2>
<p>最终移除 <code>@vue/compat</code> 后，生产包体为 17566.2 kB（未压缩）和 8569.6 kB（gzip），相对基线增长
14.5% 和 15.8%，超过了最初设定的 ±10% 目标。</p>
<p>分析结果表明，这不是一次偶然的打包回归：本次刻意保持 Element Plus 全量引入以控制迁移范围，而
VXE Table 4 的运行时也更重。若要把体积拉回预算，需要单独规划 Element Plus 按需引入和相关验证，
而不是在迁移收尾时混入一次性能优化。把“功能迁移正确”与“下一阶段的优化机会”分开记录，能让交付
结论保持诚实，也不会掩盖后续工作。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="这次迁移留下的做法">这次迁移留下的做法<a href="https://blog.biulight.top/timeline/blog/vue2-to-vue3-migration#%E8%BF%99%E6%AC%A1%E8%BF%81%E7%A7%BB%E7%95%99%E4%B8%8B%E7%9A%84%E5%81%9A%E6%B3%95" class="hash-link" aria-label="这次迁移留下的做法的直接链接" title="这次迁移留下的做法的直接链接" translate="no">​</a></h2>
<ol>
<li class="">先定义不变项：本次不改业务逻辑、不换状态管理、不换构建器，避免范围膨胀。</li>
<li class="">让兼容层只做临时诊断工具，并把移除它作为验收条件。</li>
<li class="">将可机械化的修改和需要人工判断的修改分开管理，拒绝“一把梭”的全局替换。</li>
<li class="">用登录、列表、表单、详情和弹窗建立真实冒烟基线；构建通过不是功能通过。</li>
<li class="">每一类迁移差异都记录成“现象、根因、修复规则、回归场景”，让后续维护不必重新踩坑。</li>
</ol>
<p>Vue 2 到 Vue 3 的迁移不是为了把所有组件写成 <code>&lt;script setup&gt;</code>。对一个正在运行的业务系统来说，
更有价值的结果是：应用在原生 Vue 3 运行时上稳定交付，关键交互得到验证，且每一条兼容性债务都有
明确的边界和去向。</p>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="Vue" term="Vue"/>
        <category label="Vue 2" term="Vue 2"/>
        <category label="Vue 3" term="Vue 3"/>
        <category label="Element Plus" term="Element Plus"/>
        <category label="Rsbuild" term="Rsbuild"/>
        <category label="Migration" term="Migration"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[给 Nginx UI 配置 HTTPS：从界面申请失败到拆开证书签发链路]]></title>
        <id>https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate</id>
        <link href="https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate"/>
        <updated>2026-07-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[这次原本只想给一个已经由 Nginx UI 管理的站点加上 HTTPS，最后却依次排查了]]></summary>
        <content type="html"><![CDATA[<p>这次原本只想给一个已经由 Nginx UI 管理的站点加上 HTTPS，最后却依次排查了
Nginx UI 启动、ACME CA、域名验证、DNS API 权限、证书文件和 Nginx 重载。</p>
<p>最有价值的结论不是某一条“万能命令”，而是要把问题拆成四段：管理界面能否工作、CA
能否创建订单、域名控制权能否验证、Nginx 最终是否加载了正确的证书。把这四件事混在
Nginx UI 的一个申请按钮里看，很容易在错误方向上反复尝试。</p>
<p>本文保留这次排查的时间线，但域名、凭据和服务器信息均已替换为占位符。可直接执行的
完整流程放在<a class="" href="https://blog.biulight.top/timeline/knowledge/nginx-ui-https-certificate">使用 acme.sh 为 Nginx UI 管理的站点配置 HTTPS</a>。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="起点先遇到的不是证书而是-nginx-ui-启动">起点：先遇到的不是证书，而是 Nginx UI 启动<a href="https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E8%B5%B7%E7%82%B9%E5%85%88%E9%81%87%E5%88%B0%E7%9A%84%E4%B8%8D%E6%98%AF%E8%AF%81%E4%B9%A6%E8%80%8C%E6%98%AF-nginx-ui-%E5%90%AF%E5%8A%A8" class="hash-link" aria-label="起点：先遇到的不是证书，而是 Nginx UI 启动的直接链接" title="起点：先遇到的不是证书，而是 Nginx UI 启动的直接链接" translate="no">​</a></h2>
<p>最初看到 Nginx UI 无法正常启动时，我先把它和证书问题联系到了一起。后来才意识到，
Nginx UI 进程、Nginx 进程和 ACME 签发是三条相互关联但可以独立验证的链路。</p>
<p>排查顺序应该先回到最基础的状态：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">systemctl status nginx-ui --no-pager</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">journalctl -u nginx-ui -n 100 --no-pager</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">nginx -t</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">systemctl status nginx --no-pager</span><br></span></code></pre></div></div>
<p>只有 Nginx UI 和 Nginx 本身都稳定后，证书申请日志才有解释价值。否则一次界面上的失败，
可能只是服务未启动、配置未加载或 Nginx 测试未通过，并不代表 CA 拒绝签发。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="第一次弯路在-nginx-ui-里直接配置证书">第一次弯路：在 Nginx UI 里直接配置证书<a href="https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E7%AC%AC%E4%B8%80%E6%AC%A1%E5%BC%AF%E8%B7%AF%E5%9C%A8-nginx-ui-%E9%87%8C%E7%9B%B4%E6%8E%A5%E9%85%8D%E7%BD%AE%E8%AF%81%E4%B9%A6" class="hash-link" aria-label="第一次弯路：在 Nginx UI 里直接配置证书的直接链接" title="第一次弯路：在 Nginx UI 里直接配置证书的直接链接" translate="no">​</a></h2>
<p>服务恢复后，我尝试直接通过 Nginx UI 申请和配置证书。这条路径本身是受支持的，但在这次
环境里，它同时引入了太多变量：</p>
<ul>
<li class="">使用 Let’s Encrypt 还是 ZeroSSL；</li>
<li class="">选择 HTTP-01 还是 DNS-01；</li>
<li class="">域名的权威 DNS 到底由 DNSPod 还是阿里云管理；</li>
<li class="">DNS API 凭据是否有权创建和删除 <code>_acme-challenge</code> TXT 记录；</li>
<li class="">系统递归 DNS 是否已经看到新记录；</li>
<li class="">证书与私钥是否以 Nginx UI 能识别的方式落盘。</li>
</ul>
<p>这次对话中的日志先后指向不同环节。切换 CA 后错误表现会变化，但这并不能修复域名验证；
DNS 页面能登录也不代表 API 凭据有修改记录的权限；浏览器能打开域名，也不代表 ACME
验证节点能获得期望的 TXT 或 HTTP 响应。</p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="容易误读的端口">容易误读的端口<a href="https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E5%AE%B9%E6%98%93%E8%AF%AF%E8%AF%BB%E7%9A%84%E7%AB%AF%E5%8F%A3" class="hash-link" aria-label="容易误读的端口的直接链接" title="容易误读的端口的直接链接" translate="no">​</a></h3>
<p>Nginx UI 的 <code>HTTPChallengePort</code> 是其后端在 HTTP-01 流程中使用的监听端口，当前文档中的
默认值是 <code>9180</code>。但 Let’s Encrypt 的 HTTP-01 验证仍然只会从公网访问 80 端口。也就是说，
仅确认 <code>9180</code> 在本机监听并不能证明验证链路成立，公网 80 端口还必须把
<code>/.well-known/acme-challenge/</code> 请求正确交给挑战处理程序。</p>
<p>这也是直接配置时最容易出现的错觉：界面和后端都在运行，CA 却仍然无法验证域名。</p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="容易误读的证书已存在">容易误读的“证书已存在”<a href="https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E5%AE%B9%E6%98%93%E8%AF%AF%E8%AF%BB%E7%9A%84%E8%AF%81%E4%B9%A6%E5%B7%B2%E5%AD%98%E5%9C%A8" class="hash-link" aria-label="容易误读的“证书已存在”的直接链接" title="容易误读的“证书已存在”的直接链接" translate="no">​</a></h3>
<p>另一个坑是文件形态。Nginx UI 的自动发现会扫描配置的 Nginx <code>ssl</code> 目录，并识别分别保存的
证书链和私钥，例如 <code>fullchain.pem</code> 与 <code>privkey.pem</code>。把证书链和私钥拼成单个 PEM 文件，
并不属于它的自动发现范围。</p>
<p>因此，“服务器上已经有一个 PEM 文件”并不等于 Nginx UI 已经发现它，更不等于站点配置
已经引用它。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="改变策略把签发和部署拆开">改变策略：把签发和部署拆开<a href="https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E6%94%B9%E5%8F%98%E7%AD%96%E7%95%A5%E6%8A%8A%E7%AD%BE%E5%8F%91%E5%92%8C%E9%83%A8%E7%BD%B2%E6%8B%86%E5%BC%80" class="hash-link" aria-label="改变策略：把签发和部署拆开的直接链接" title="改变策略：把签发和部署拆开的直接链接" translate="no">​</a></h2>
<p>继续在界面里同时调整 CA、挑战方式和 DNS 凭据只会让日志越来越难比较。我最后改用
<code>acme.sh</code> 单独完成 DNS-01 签发，再把稳定的证书文件交给 Nginx UI 和 Nginx 使用。</p>
<p>选择 DNS-01 的原因不是它天然优于 HTTP-01，而是这次环境中的公网 80 端口和转发链路并不
适合作为唯一验证入口。DNS-01 直接通过 <code>_acme-challenge.&lt;DOMAIN&gt;</code> TXT 记录证明域名控制权，
也支持通配符证书。</p>
<p>在执行前，先确认权威 DNS，而不是根据域名注册商或曾经使用过的控制台猜测：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">dig NS example.com +short</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dig TXT _acme-challenge.app.example.com +short</span><br></span></code></pre></div></div>
<p>然后只为实际的权威 DNS 提供商创建最小权限凭据。本次最终可工作的链路是：</p>
<ol>
<li class=""><code>acme.sh</code> 向选定的 ACME CA 创建订单；</li>
<li class="">DNS API 临时创建 <code>_acme-challenge</code> TXT 记录；</li>
<li class="">CA 查询权威 DNS 并完成验证；</li>
<li class=""><code>acme.sh --install-cert</code> 把证书链和私钥复制到 Nginx 的稳定目录；</li>
<li class=""><code>nginx -t</code> 通过后重载 Nginx。</li>
</ol>
<p>CA 从 ZeroSSL 切换到 Let’s Encrypt 时，我明确写出了 <code>--server letsencrypt</code>。这一步只是
选择 CA，不应被当成修复 DNS 权限或网络问题的方法。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="为什么没有直接引用-acmesh-的内部文件">为什么没有直接引用 acme.sh 的内部文件<a href="https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B2%A1%E6%9C%89%E7%9B%B4%E6%8E%A5%E5%BC%95%E7%94%A8-acmesh-%E7%9A%84%E5%86%85%E9%83%A8%E6%96%87%E4%BB%B6" class="hash-link" aria-label="为什么没有直接引用 acme.sh 的内部文件的直接链接" title="为什么没有直接引用 acme.sh 的内部文件的直接链接" translate="no">​</a></h2>
<p><code>acme.sh</code> 会在 <code>~/.acme.sh/</code> 下保存自己的状态和证书材料，但其官方说明明确不建议服务器
直接引用这些内部文件。正确做法是使用 <code>--install-cert</code> 复制到生产路径，并配置重载命令：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">~/.acme.sh/acme.sh --install-cert -d app.example.com --ecc \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  --key-file /etc/nginx/ssl/app.example.com/privkey.pem \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  --fullchain-file /etc/nginx/ssl/app.example.com/fullchain.pem \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  --reloadcmd "nginx -t &amp;&amp; systemctl reload nginx"</span><br></span></code></pre></div></div>
<p>这样续期后会再次更新稳定路径，并在配置检查通过后重载 Nginx。若只复制一次文件，证书虽然
会续期，线上服务却可能一直展示旧证书。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="最后如何确认真的成功">最后如何确认真的成功<a href="https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E6%9C%80%E5%90%8E%E5%A6%82%E4%BD%95%E7%A1%AE%E8%AE%A4%E7%9C%9F%E7%9A%84%E6%88%90%E5%8A%9F" class="hash-link" aria-label="最后如何确认真的成功的直接链接" title="最后如何确认真的成功的直接链接" translate="no">​</a></h2>
<p>浏览器出现锁形图标只是最后一个信号。我把验证拆成了四层：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">dig A app.example.com +short</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ss -lntp | rg ':(80|443)\b'</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">nginx -t</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">openssl s_client -connect app.example.com:443 \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  -servername app.example.com -showcerts &lt;/dev/null</span><br></span></code></pre></div></div>
<p>还要核对证书和私钥是否匹配。下面的公钥摘要应相同：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">openssl x509 -in /etc/nginx/ssl/app.example.com/fullchain.pem \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  -pubkey -noout | openssl sha256</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">openssl pkey -in /etc/nginx/ssl/app.example.com/privkey.pem \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  -pubout | openssl sha256</span><br></span></code></pre></div></div>
<p>最后执行一次不强制续期的定时任务检查，并查看下一次续期时间：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">~/.acme.sh/acme.sh --cron --home ~/.acme.sh</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">~/.acme.sh/acme.sh --info -d app.example.com --ecc</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="这次踩坑留下的判断顺序">这次踩坑留下的判断顺序<a href="https://blog.biulight.top/timeline/blog/nginx-ui-https-certificate#%E8%BF%99%E6%AC%A1%E8%B8%A9%E5%9D%91%E7%95%99%E4%B8%8B%E7%9A%84%E5%88%A4%E6%96%AD%E9%A1%BA%E5%BA%8F" class="hash-link" aria-label="这次踩坑留下的判断顺序的直接链接" title="这次踩坑留下的判断顺序的直接链接" translate="no">​</a></h2>
<p>以后再遇到同类问题，我会按这个顺序处理：</p>
<ol>
<li class="">先确认 Nginx UI 与 Nginx 服务健康，避免把启动问题当成证书问题。</li>
<li class="">查权威 DNS，再选择 DNS API，不能根据注册商或控制台印象猜。</li>
<li class="">一次只改变一个变量；切换 CA 不会修复挑战验证。</li>
<li class="">区分“签发成功”“文件已部署”和“Nginx 已加载”三个状态。</li>
<li class="">使用稳定部署路径和可靠的 <code>reloadcmd</code>，把自动续期真正闭环。</li>
</ol>
<p>完整的无敏感信息操作清单和回滚方法见：</p>
<ul>
<li class=""><a class="" href="https://blog.biulight.top/timeline/knowledge/nginx-ui-https-certificate">使用 acme.sh 为 Nginx UI 管理的站点配置 HTTPS</a></li>
</ul>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="Nginx UI" term="Nginx UI"/>
        <category label="HTTPS" term="HTTPS"/>
        <category label="ACME" term="ACME"/>
        <category label="Troubleshooting" term="Troubleshooting"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[新增了一条 CoreDNS 记录却解析不出来：从怀疑 Surge、Proxifier 到重启 mDNSResponder]]></title>
        <id>https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache</id>
        <link href="https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache"/>
        <updated>2026-07-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[给自建的私有域名新增了一条 CoreDNS 记录，ping 却始终报"无法解析主机"，而同一个域名后缀下]]></summary>
        <content type="html"><![CDATA[<p>给自建的私有域名新增了一条 CoreDNS 记录，<code>ping</code> 却始终报"无法解析主机"，而同一个域名后缀下
其他早就存在的记录一切正常。这次排查依次怀疑了 Surge、Proxifier，最后才发现是 macOS
<code>mDNSResponder</code> 的陈旧缓存在作怪。</p>
<p>本文保留这次排查的时间线，域名、内网地址和服务器信息均已替换为占位符。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="背景">背景<a href="https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E8%83%8C%E6%99%AF" class="hash-link" aria-label="背景的直接链接" title="背景的直接链接" translate="no">​</a></h2>
<p>设备已经通过 <code>shine sys apply split-dns</code> 把私有域名后缀交给自建 CoreDNS 解析,具体原理见
<a class="" href="https://blog.biulight.top/timeline/knowledge/zerotier-coredns-split-dns">使用 ZeroTier、CoreDNS 和 Shine 搭建异地私有域名网络</a>。
这次是往 CoreDNS 的 <code>Corefile</code> 里新增了一条 <code>hosts</code> 记录：</p>
<div class="language-text codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-text codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">hosts {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    10.20.30.10 nginx-ui.example.internal</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    10.20.30.10 blog.example.internal   # 新加的</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    fallthrough</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre></div></div>
<p><code>reload</code> 插件会自动感知 <code>Corefile</code> 变化，理论上不需要重启容器。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="现象">现象<a href="https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E7%8E%B0%E8%B1%A1" class="hash-link" aria-label="现象的直接链接" title="现象的直接链接" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">❯ ping blog.example.internal</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ping: cannot resolve blog.example.internal: Unknown host</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">❯ ping nginx-ui.example.internal</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">PING nginx-ui.example.internal (10.20.30.10): 56 data bytes</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">64 bytes from 10.20.30.10: icmp_seq=0 ttl=64 time=11.383 ms</span><br></span></code></pre></div></div>
<p>同一个域名后缀、同一台 CoreDNS、同一个目标 IP，新记录解析不出来，旧记录完全正常。这种"只有
新东西不行"的现象,比单纯的"DNS 全部失效"更容易让人误判方向。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="先排除服务端">先排除服务端<a href="https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E5%85%88%E6%8E%92%E9%99%A4%E6%9C%8D%E5%8A%A1%E7%AB%AF" class="hash-link" aria-label="先排除服务端的直接链接" title="先排除服务端的直接链接" translate="no">​</a></h2>
<p>在下结论之前，先直接绕过本机的解析路径，对 CoreDNS 发一次权威查询：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">dig @10.20.30.10 blog.example.internal</span><br></span></code></pre></div></div>
<p>拿到的是正确、带 <code>aa</code>（authoritative answer）标志的应答。这一步很关键：它证明 CoreDNS 本身
配置正确、<code>hosts</code> 记录也确实生效了，问题出在客户端到 CoreDNS 之间的某个环节，而不是服务端。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="第一个怀疑对象surge">第一个怀疑对象：Surge<a href="https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%80%80%E7%96%91%E5%AF%B9%E8%B1%A1surge" class="hash-link" aria-label="第一个怀疑对象：Surge的直接链接" title="第一个怀疑对象：Surge的直接链接" translate="no">​</a></h2>
<p><code>scutil --dns</code> 显示 split-dns 的域名专属 resolver 配置完全正确，指向 CoreDNS 地址；但
<code>dscacheutil -q host -a name blog.example.internal</code>（真正走系统解析路径的检查方式）却什么
都拿不到。同时 <code>route -n get 10.20.30.10</code> 显示去 CoreDNS 的路由走的是一个 <code>feth</code> 虚拟网卡，
而这台机器上 Surge 的 Network Extension 处于 <code>activated enabled</code> 状态，很自然地怀疑是 Surge
的 Enhanced Mode（TUN 模式）把 DNS 流量整体接管了。</p>
<p>但这个方向被用户当场否掉：Surge 既没开系统代理，也没开 TUN 模式，只是用来暴露一个代理接口。
后面证实这个怀疑确实是误判——<code>feth</code> 网卡另有来源，和 Surge 是否处于 Enhanced Mode 无关。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="第二个怀疑对象proxifier">第二个怀疑对象：Proxifier<a href="https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E7%AC%AC%E4%BA%8C%E4%B8%AA%E6%80%80%E7%96%91%E5%AF%B9%E8%B1%A1proxifier" class="hash-link" aria-label="第二个怀疑对象：Proxifier的直接链接" title="第二个怀疑对象：Proxifier的直接链接" translate="no">​</a></h2>
<p>同一台机器上还装了 Proxifier，它的系统扩展同样是 <code>activated enabled</code>。检查它当前生效的
profile，发现关键设置：</p>
<div class="language-xml codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-xml codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">Resolve</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">ViaProxy</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">enabled</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">true</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">TryLocalDnsFirst</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">enabled</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">false</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">ViaProxy</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">ExclusionList</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">...</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain">localhost;%SimpleHostnames%;%ComputerName%;*.local;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">ExclusionList</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">Resolve</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></span></code></pre></div></div>
<p><code>ViaProxy enabled="true"</code> 加 <code>TryLocalDnsFirst enabled="false"</code> 意味着：只要域名没有命中
排除列表，Proxifier 就不会走本地 DNS，而是尝试通过代理解析——代理服务器当然不认识私有域名。
排除列表里只有 <code>*.local</code>，没有内网域名后缀，这个方向看起来很有说服力。</p>
<p>第一次修改却改错了地方：把内网域名后缀加进了 <code>RuleList</code> 里某条规则的 <code>Targets</code>（那是连接
路由规则，Direct/Proxy 二选一），而不是 <code>Options → Resolve</code> 里控制"域名解析是否走代理"的
<code>ExclusionList</code>——域名解析在连接路由匹配之前就已经失败，规则改了也没用。</p>
<p>修正到正确的位置后仍然不行；干脆完全退出 Proxifier 应用再测试，问题依旧存在。到这一步，
Proxifier 也被排除了。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="真正的原因mdnsresponder-的陈旧缓存">真正的原因：mDNSResponder 的陈旧缓存<a href="https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E7%9C%9F%E6%AD%A3%E7%9A%84%E5%8E%9F%E5%9B%A0mdnsresponder-%E7%9A%84%E9%99%88%E6%97%A7%E7%BC%93%E5%AD%98" class="hash-link" aria-label="真正的原因：mDNSResponder 的陈旧缓存的直接链接" title="真正的原因：mDNSResponder 的陈旧缓存的直接链接" translate="no">​</a></h2>
<p>排除了两个代理工具之后，回到最基础的怀疑对象——本机 DNS 解析进程自身的缓存。</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">dscacheutil -flushcache</span><br></span></code></pre></div></div>
<p>在近几个版本的 macOS 上，这条命令并不总能可靠地清掉 <code>mDNSResponder</code> 内部持有的解析结果，
必须直接让守护进程重新加载：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo killall -HUP mDNSResponder</span><br></span></code></pre></div></div>
<p>执行后立刻复测：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">❯ ping blog.example.internal</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">PING blog.example.internal (10.20.30.10): 56 data bytes</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">64 bytes from 10.20.30.10: icmp_seq=0 ttl=64 time=19.909 ms</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">64 bytes from 10.20.30.10: icmp_seq=1 ttl=64 time=11.723 ms</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">❯ dscacheutil -q host -a name blog.example.internal</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">name: blog.example.internal</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ip_address: 10.20.30.10</span><br></span></code></pre></div></div>
<p>问题解决，和 Surge、Proxifier、CoreDNS 配置都没有关系。</p>
<p>真正的原因是：<code>blog.example.internal</code> 是后加进 CoreDNS 的新记录，而 <code>nginx-ui.example.internal</code>
这类记录加入的时间更早。大概率是在这条新记录刚加入、split-dns 还没有完全生效的窗口期内，
<code>mDNSResponder</code> 已经对新名字做过一次失败查询并缓存了负面结果；而老记录当时早已缓存了正确
结果，所以一直"看起来没问题"，制造了"只有新东西解析不出来"的假象，把排查方向一路带偏到了
两个代理工具身上。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="这次排查留下的判断顺序">这次排查留下的判断顺序<a href="https://blog.biulight.top/timeline/blog/coredns-new-record-mdnsresponder-cache#%E8%BF%99%E6%AC%A1%E6%8E%92%E6%9F%A5%E7%95%99%E4%B8%8B%E7%9A%84%E5%88%A4%E6%96%AD%E9%A1%BA%E5%BA%8F" class="hash-link" aria-label="这次排查留下的判断顺序的直接链接" title="这次排查留下的判断顺序的直接链接" translate="no">​</a></h2>
<p>以后再遇到"某个内网域名解析不出来，其他域名都正常"，会按这个顺序处理：</p>
<ol>
<li class="">先用 <code>dig @&lt;DNS服务器IP&gt; &lt;域名&gt;</code> 直接查权威 DNS，确认是服务端问题还是客户端问题，
不要一上来就怀疑代理软件。</li>
<li class="">拿同一个域名后缀下"早就存在、一直正常"的记录和"新加的、解析不出来"的记录做对比。如果
真的是代理或路由层面的问题，两者应该同时失效；只有新记录失效，更可能是客户端缓存问题。</li>
<li class=""><code>dscacheutil -flushcache</code> 不一定够用，<code>sudo killall -HUP mDNSResponder</code> 才是更可靠的
强制刷新方式。</li>
<li class="">用 Proxifier 一类工具时，分清"域名解析是否走代理"（Resolve/ExclusionList）和"连接走
Direct 还是走代理"（RuleList/Targets）是两个独立的配置面，改错地方不会生效，也不会报错，
容易误以为"这个方向也不对"。</li>
<li class="">新增内网 DNS 记录后，如果客户端之前已经查询过这个域名并失败过，最好主动做一次
<code>sudo killall -HUP mDNSResponder</code>，而不是假设 <code>reload</code> 插件生效后客户端会自动更新。</li>
</ol>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="macOS" term="macOS"/>
        <category label="CoreDNS" term="CoreDNS"/>
        <category label="DNS" term="DNS"/>
        <category label="Troubleshooting" term="Troubleshooting"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[终端代理没有排除 ZeroTier 私有域名，结果请求被本地代理拦截]]></title>
        <id>https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier</id>
        <link href="https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier"/>
        <updated>2026-07-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[最近在一台已经接入 ZeroTier 的设备上排查服务连通性，本来只是想用 curl 直接访问私有域名，结果请求却被本地 HTTP 代理接管了。]]></summary>
        <content type="html"><![CDATA[<p>最近在一台已经接入 ZeroTier 的设备上排查服务连通性，本来只是想用 <code>curl</code> 直接访问私有域名，结果请求却被本地 HTTP 代理接管了。</p>
<p>如果你也在终端里设置了 <code>HTTP_PROXY</code>、<code>HTTPS_PROXY</code>，同时又依赖 ZeroTier 私有域名或 <code>192.168.x.x</code> 这一类局域网地址，最好顺手检查一下 <code>NO_PROXY</code> / <code>no_proxy</code>。否则 DNS 虽然能解析到内网目标，请求仍可能先进入本地代理链路。</p>
<p>这类场景在结合 Shine 的 <code>proxy</code> shell 预设时尤其容易忽略，因为我们平时更关注“代理有没有打开”，不一定会第一时间想到“哪些地址本来就不该走代理”。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="背景">背景<a href="https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E8%83%8C%E6%99%AF" class="hash-link" aria-label="背景的直接链接" title="背景的直接链接" translate="no">​</a></h2>
<p>目标很简单：通过 ZeroTier 提供的私有网络和私有 DNS，直接访问一台内网服务。</p>
<p>按预期，这条链路应该是：</p>
<ol>
<li class="">终端解析 <code>&lt;PRIVATE_HOST&gt;</code> 之类的私有域名。</li>
<li class="">系统把该域名交给私有 DNS。</li>
<li class="">DNS 返回 ZeroTier 地址或局域网地址。</li>
<li class=""><code>curl</code> 直接连向目标服务。</li>
</ol>
<p>但实际现象不是这样。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="现象">现象<a href="https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E7%8E%B0%E8%B1%A1" class="hash-link" aria-label="现象的直接链接" title="现象的直接链接" translate="no">​</a></h2>
<p>在已经启用终端代理的 shell 中执行：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">curl http://&lt;PRIVATE_HOST&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">curl http://192.168.0.10:8080</span><br></span></code></pre></div></div>
<p>请求没有直接访问目标，而是进入了本地 HTTP 代理。对某些代理软件来说，这会表现为请求被拦截、拒绝，或者在日志里出现一条本不该经过代理的内网访问记录。</p>
<p>这时最容易误判成下面几类问题：</p>
<ul>
<li class="">ZeroTier 没连通；</li>
<li class="">CoreDNS 没解析对；</li>
<li class="">目标服务没启动；</li>
<li class="">私有域名配置有误。</li>
</ul>
<p>但如果浏览器、其他客户端，或者关闭代理后的同一条命令能够访问成功，就该反过来检查当前 shell 的代理环境。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="原因">原因<a href="https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E5%8E%9F%E5%9B%A0" class="hash-link" aria-label="原因的直接链接" title="原因的直接链接" translate="no">​</a></h2>
<p><code>curl</code> 这类命令行工具会读取当前终端中的代理变量，例如：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">env | rg '^(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)='</span><br></span></code></pre></div></div>
<p>只要 <code>HTTP_PROXY</code> / <code>HTTPS_PROXY</code> 已设置，而 <code>NO_PROXY</code> / <code>no_proxy</code> 没有覆盖私有域名后缀和局域网地址段，这些请求仍会走代理。</p>
<p>这也是 ZeroTier 场景里很容易忽略的一点：</p>
<ul>
<li class="">ZeroTier 解决的是“设备之间是否可达”；</li>
<li class="">私有 DNS 解决的是“私有域名解析到哪里”；</li>
<li class=""><code>NO_PROXY</code> 解决的是“这个请求该不该绕过终端代理”。</li>
</ul>
<p>这三个环节缺一不可。前两项都正确时，最后仍可能卡在代理变量这一层。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="修复">修复<a href="https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E4%BF%AE%E5%A4%8D" class="hash-link" aria-label="修复的直接链接" title="修复的直接链接" translate="no">​</a></h2>
<p>如果你用 Shine 管理终端代理，推荐直接维护 <code>PROXY_NO_PROXY</code>，让 Shine 统一生成 <code>NO_PROXY</code> 和 <code>no_proxy</code>。</p>
<p>先查看当前配置：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">shine env list</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">shine info shell/proxy</span><br></span></code></pre></div></div>
<p>然后把默认值和私有网络需要的排除项一起写进去。下面示例全部使用占位值：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">shine env set PROXY_NO_PROXY \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  'localhost,127.0.0.1,::1,.home.example.internal,192.168.0.0/16'</span><br></span></code></pre></div></div>
<p>这里至少建议覆盖：</p>
<ul>
<li class=""><code>localhost,127.0.0.1,::1</code></li>
<li class="">私有域名后缀，例如 <code>.home.example.internal</code></li>
<li class="">局域网地址段，例如 <code>192.168.0.0/16</code></li>
</ul>
<p>修改后不要只停留在 <code>env set</code>。Shine 会先把已安装的 <code>proxy</code> 预设标记为可更新，还需要执行：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">shine update shell/proxy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">shine upgrade shell/proxy</span><br></span></code></pre></div></div>
<p>如果你不是用 Shine 管理终端代理，本质上也一样：最终要确认当前 shell 里的 <code>NO_PROXY</code> / <code>no_proxy</code> 已经包含这些私有目标。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="验证">验证<a href="https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E9%AA%8C%E8%AF%81" class="hash-link" aria-label="验证的直接链接" title="验证的直接链接" translate="no">​</a></h2>
<p>先确认当前 shell 中的代理变量已经变成预期值：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">shine info shell/proxy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">env | rg '^(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)='</span><br></span></code></pre></div></div>
<p>再分别验证私有域名和局域网地址：</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">curl http://&lt;PRIVATE_HOST&gt;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">curl http://192.168.0.10:8080</span><br></span></code></pre></div></div>
<p>如果修复生效，这两类请求应该直接访问目标，而不是继续进入本地代理。</p>
<p>若你使用的是基于私有域名的 ZeroTier 解析链路，也可以顺手对照这篇原理说明：</p>
<ul>
<li class=""><a class="" href="https://blog.biulight.top/timeline/knowledge/zerotier-coredns-split-dns">使用 ZeroTier、CoreDNS 和 Shine 搭建异地私有域名网络</a></li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="延伸建议">延伸建议<a href="https://blog.biulight.top/timeline/blog/terminal-proxy-no-proxy-zerotier#%E5%BB%B6%E4%BC%B8%E5%BB%BA%E8%AE%AE" class="hash-link" aria-label="延伸建议的直接链接" title="延伸建议的直接链接" translate="no">​</a></h2>
<p>这个坑本质上不是 ZeroTier 独有问题，而是“终端代理”和“私有网络”叠在一起时的常见遗漏。</p>
<p>如果你希望以后少踩一次，最简单的做法有两个：</p>
<ol>
<li class="">把私有域名后缀和常用内网地址段纳入 <code>PROXY_NO_PROXY</code> 的默认维护范围。</li>
<li class="">每次怀疑 ZeroTier 或私有 DNS 之前，先检查一眼当前 shell 的代理变量。</li>
</ol>
<p>如果你只想快速排查同类问题，可以直接看这篇排障文：</p>
<ul>
<li class=""><a class="" href="https://blog.biulight.top/timeline/knowledge/terminal-proxy-no-proxy-zerotier">终端代理误拦截 ZeroTier 私有域名的排查与修复</a></li>
</ul>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="Shine" term="Shine"/>
        <category label="ZeroTier" term="ZeroTier"/>
        <category label="Proxy" term="Proxy"/>
        <category label="Troubleshooting" term="Troubleshooting"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[拯救你的Docker]]></title>
        <id>https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker</id>
        <link href="https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker"/>
        <updated>2025-02-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[威联通 Docker 国内无法拉取镜像？]]></summary>
        <content type="html"><![CDATA[<p>威联通 Docker 国内无法拉取镜像？</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="背景">背景<a href="https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E8%83%8C%E6%99%AF" class="hash-link" aria-label="背景的直接链接" title="背景的直接链接" translate="no">​</a></h2>
<p>由于众所周知的原因，国内拉取 docker 镜像愈发可困难，网上现有的教程涉及 NAS 的极少，为了能愉快的使用 NAS，我进行了大量尝试，终于找到了可用的方法，顺手记录下，分享给各位苦苦挣扎的道友。</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="常见的方案">常见的方案<a href="https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E5%B8%B8%E8%A7%81%E7%9A%84%E6%96%B9%E6%A1%88" class="hash-link" aria-label="常见的方案的直接链接" title="常见的方案的直接链接" translate="no">​</a></h2>
<p>现有环境下，想成功拉取镜像，无非使用魔法或者换国内镜像。</p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="使用代理">使用代理<a href="https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E4%BD%BF%E7%94%A8%E4%BB%A3%E7%90%86" class="hash-link" aria-label="使用代理的直接链接" title="使用代理的直接链接" translate="no">​</a></h3>
<ol>
<li class="">开启威联通 ssh 服务,并通过 ssh 连接到 QNAP</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain"># 远程登录</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ssh -p 22 username@192.168.0.155</span><br></span></code></pre></div></div>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><p>SSH 服务的默认端口是 22，上述命令是 用户通过 SSH 工具用账号（username）连接到服务器（192.168.0.155）的 22 端口</p></div></div>
<ol start="2">
<li class="">增加 docker 的代理配置</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain"># 编辑配置文件</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sudo vim /share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json</span><br></span></code></pre></div></div>
<p>增加代理配置</p>
<div class="language-json codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_X2iu">/share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json</div><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-json codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 新增配置</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"proxies"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"http-proxy"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"http://代理地址:端口"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"https-proxy"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"http://代理地址:端口"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"no-proxy"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"*.test.example.com,.example.org,127.0.0.0/8"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><p>ssh 登录后，可以通过 <code>which docker</code>、<code>whereis docker</code> 查看 docker 所在位置</p></div></div>
<ol start="3">
<li class="">重启 docker 服务</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo /etc/init.d/container-station.sh restart</span><br></span></code></pre></div></div>
<ol start="4">
<li class="">查看配置是否成功</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">docker info</span><br></span></code></pre></div></div>
<p>如下图：
<img decoding="async" loading="lazy" alt="代理设置成功" src="https://blog.biulight.top/timeline/assets/images/docker-info-dd1c7cccfc8c2e18e2e04eb3ccf43644.jpg" width="1334" height="620" class="img_oexP"></p>
<p>完成上述设置后，你就可以愉快的拉取镜像了</p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="更换国内镜像源">更换国内镜像源<a href="https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E6%9B%B4%E6%8D%A2%E5%9B%BD%E5%86%85%E9%95%9C%E5%83%8F%E6%BA%90" class="hash-link" aria-label="更换国内镜像源的直接链接" title="更换国内镜像源的直接链接" translate="no">​</a></h3>
<blockquote>
<p>参考<strong>使用代理</strong>，增加镜像源配置</p>
</blockquote>
<p>步骤 1、2 参考<a href="https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E4%BD%BF%E7%94%A8%E4%BB%A3%E7%90%86" class="">使用代理</a></p>
<div class="language-text codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-text codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain"># 编辑配置文件</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sudo vim /share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json</span><br></span></code></pre></div></div>
<p>增加镜像源配置</p>
<div class="language-json codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_X2iu">/share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json</div><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-json codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 新增配置</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"registry-mirrors"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"https://mirror.ccs.tencentyun.com"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"https://docker.mirrors.ustc.edu.cn"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<div class="theme-admonition theme-admonition-info admonition_FTDX alert alert--info"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>信息</div><div class="admonitionContent_ESmc"><p>腾讯云镜像加速器地址：<a href="https://mirror.ccs.tencentyun.com/" target="_blank" rel="noopener noreferrer" class="">https://mirror.ccs.tencentyun.com</a></p><p>中国科学技术大学：<a href="https://docker.mirrors.ustc.edu.cn/" target="_blank" rel="noopener noreferrer" class="">https://docker.mirrors.ustc.edu.cn</a></p><p>Docker 官方镜像（中国区）镜像加速：<a href="https://registry.docker-cn.com/" target="_blank" rel="noopener noreferrer" class="">https://registry.docker-cn.com</a></p><p>网易云镜像加速器地址：<a href="http://hub-mirror.c.163.com/" target="_blank" rel="noopener noreferrer" class="">http://hub-mirror.c.163.com</a></p><p>南京大学镜像加速器地址：<a href="https://docker.nju.edu.cn/" target="_blank" rel="noopener noreferrer" class="">https://docker.nju.edu.cn</a></p></div></div>
<p>剩余步骤参考<a href="https://blog.biulight.top/timeline/blog/2025/02/07/qnap-docker#%E4%BD%BF%E7%94%A8%E4%BB%A3%E7%90%86" class="">使用代理</a></p>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="Linux" term="Linux"/>
        <category label="Docker" term="Docker"/>
        <category label="QNAP" term="QNAP"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[使用Github自动交付前端制品]]></title>
        <id>https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github</id>
        <link href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github"/>
        <updated>2025-02-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[怎样通过 Github 自动交付前端编译产物？]]></summary>
        <content type="html"><![CDATA[<p>怎样通过 Github 自动交付前端编译产物？</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="准备">准备<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E5%87%86%E5%A4%87" class="hash-link" aria-label="准备的直接链接" title="准备的直接链接" translate="no">​</a></h2>
<p>一台阿里云 <code>ubuntu</code> 服务器</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="实施">实施<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E5%AE%9E%E6%96%BD" class="hash-link" aria-label="实施的直接链接" title="实施的直接链接" translate="no">​</a></h2>
<p><strong>以 <code>deploy</code> 用户组下的 <code>deploy001-timeline</code> 用户,部署目录 <code>/path/to/folder</code>，部署工具 <code>easingthemes/ssh-deploy@v2.2.11</code> 为例</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="新建部署用户修改部署目录权限">新建部署用户，修改部署目录权限<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E6%96%B0%E5%BB%BA%E9%83%A8%E7%BD%B2%E7%94%A8%E6%88%B7%E4%BF%AE%E6%94%B9%E9%83%A8%E7%BD%B2%E7%9B%AE%E5%BD%95%E6%9D%83%E9%99%90" class="hash-link" aria-label="新建部署用户，修改部署目录权限的直接链接" title="新建部署用户，修改部署目录权限的直接链接" translate="no">​</a></h3>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><p>遵循权限最小化原则，创建 <code>deploy001-timeline</code> 用户，用于部署
若已存在 <code>deploy</code> 用户组，跳过<strong>步骤 1</strong>，直接新建指定项目部署用户</p></div></div>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain"># 检查指定xxx用户组是否存在</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">cat /etc/group | grep xxx</span><br></span></code></pre></div></div>
<ol>
<li class="">创建用户组</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo addgroup deploy</span><br></span></code></pre></div></div>
<ol start="2">
<li class="">创建用户</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo adduser --ingroup deploy deploy001-timeline</span><br></span></code></pre></div></div>
<ol start="3">
<li class="">指定部署目录的归属</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo chown deploy001-timeline:deploy /path/to/folder</span><br></span></code></pre></div></div>
<ol start="4">
<li class="">更新目录权限</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo chmod 755 /path/to/folder</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="配置部署工具">配置部署工具<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E9%85%8D%E7%BD%AE%E9%83%A8%E7%BD%B2%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="配置部署工具的直接链接" title="配置部署工具的直接链接" translate="no">​</a></h3>
<p><strong>以 <code>easingthemes/ssh-deploy@v2.2.11</code> 为例</strong></p>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><p><a href="https://github.com/easingthemes/ssh-deploy" target="_blank" rel="noopener noreferrer" class="">easingthemes/ssh-deploy</a>使用手册</p></div></div>
<h4 class="anchor anchorTargetStickyNavbar_dDPi" id="服务器侧">服务器侧<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%BE%A7" class="hash-link" aria-label="服务器侧的直接链接" title="服务器侧的直接链接" translate="no">​</a></h4>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><p>使用 <code>ssh</code> 登录服务器，需要使用一组秘钥对，其中公钥设置到服务器 <code>~/.ssh/authorized_keys</code> 文件中，私钥设置在客户端</p></div></div>
<ol>
<li class="">登录新创建的用户</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">su deploy001-timeline</span><br></span></code></pre></div></div>
<ol start="2">
<li class="">生成非对称密钥</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">ssh-keygen -m PEM -t rsa -b 4096 -f deploy001-timeline -C "deploy001-timeline"</span><br></span></code></pre></div></div>
<ol start="3">
<li class="">把公钥粘贴到 <code>authorized_keys</code> 文件中</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain"># 查看公钥，复制输出到终端的结果</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">cat deploy001-timeline.pub</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># 编辑 authorized_keys 文件，并把复制的结果粘贴到此文件</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vim ~/.ssh/authorized_keys</span><br></span></code></pre></div></div>
<div class="language-txt codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_X2iu">~/.ssh/authorized_keys</div><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-txt codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDcGxPtreCc5bBbaN7jIRuVuTQkC0Mp6VxnyC5sQKG1IZwSL5agL3Zdbu3YVzx0rSU33XZZj5lWp0AAz4ABQGhAU6iIgDNjRmhyuLG4VOS3Gas21Si1lK4Dm/D0lBA//zFmDBTwa9sAEl39rJQCQYL3n2prMrlC2k8m93RGLFmgxWDcb2D+LGgi43bgR4TXvJD6dZa7xfIldsQL5SQpCzEMX8uDDlnnsus99AqmmMzUenW6I25G2ApwgUY/Toj/scCGeHRb7PmGK0AwjqBr9siiIRNi0DpX4JbYPqLPn0IciC70RNLs2tOEFV1BwP50gmF5eDD4tI7NOLaI4HvkesYy9xF6qBtNJOfALgu7Dt+s5g5izXg2pfb/RKJw2WEj56hLI2NIOr1CgZRJH6Yq2xlP1SKlo3GwkzT/NhnhiO1NFy/FKFj/sYmn4/SM92QM2OmruPDAU6JUHUT9P7m/bhlAxo5xrzMqH8v9c4acxUGz5s87j8eNcNfaIuzMQOG+jzEOqqdWI4uLeTPHrm4pSuZUuse7IenTN3Wx1MD4CfjyZZZIWR6Xv+f7MJ0CpbvpZ7E4MjenLfb8g/upl2L1kv3Lz/UUbL4n38DCY113wkJY5x6T54ckqHUmhirsL2SCHb71VAtxUhEByCTBdPEN18Hh+QOKKTp4rmJYjNrvm4E5yw== deploy001-timeline</span><br></span></code></pre></div></div>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><p>vim 是 linux 常用的终端编辑器，常见命令如下</p><ol>
<li class=""><code>:wq</code> 保存并退出</li>
<li class=""><code>i</code> 进入输入视图</li>
</ol></div></div>
<h4 class="anchor anchorTargetStickyNavbar_dDPi" id="github-配置">Github 配置<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#github-%E9%85%8D%E7%BD%AE" class="hash-link" aria-label="Github 配置的直接链接" title="Github 配置的直接链接" translate="no">​</a></h4>
<p>配置 <code>secrets</code> ，在 workflow 中使用</p>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><p>配置位置如下图所示
<img decoding="async" loading="lazy" alt="配置位置参考图" src="https://blog.biulight.top/timeline/assets/images/deploy-051e4ae18f9c956904b921a85089bfc1.jpg" width="2774" height="1630" class="img_oexP"></p></div></div>
<ol>
<li class="">新增 <code>ALY_USER</code>，如下</li>
</ol>
<div class="language-txt codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-txt codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">deploy001-timeline</span><br></span></code></pre></div></div>
<ol start="2">
<li class="">新增 <code>ALY_HOST</code>，如下</li>
</ol>
<div class="language-txt codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-txt codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">192.168.0.155</span><br></span></code></pre></div></div>
<ol start="3">
<li class="">新增 <code>ALY_SSH_TOKEN</code>，如下</li>
</ol>
<div class="language-txt codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-txt codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">-----BEGIN RSA PRIVATE KEY-----</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">MIIJKQIBAAKCAgEA3BsT7a3gnOWwW2je4yEblbk0JAtDKelcZ8gubEChtSGcEi+W</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">oC92XW7t2Fc8dK0lN912WY+ZVqdAAM+AAUBoQFOoiIAzY0ZocrixuFTktxmrNtUo</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">tZSuA5vw9JQQP/8xZgwU8GvbABJd/ayUAkGC959qazK5QtpPJvd0RixZoMVg3G9g</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">/ixoIuN24EeE17yQ+nWWu8XyJXbEC+UkKQsxDF/Lgw5Z57LrPfQKppjM1Hp1uiNu</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">RtgKcIFGP06I/7HAhnh0W+z5hitAMI6ga/bIoiETYtA6V+CW2D6iz59CHIgu9ETS</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">7NrThBVdQcD+dIJheXgw+LSOzTi2iOB75HrGMvcReqgbTSTnwC4Luw7frOYOYs14</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">NqX2/0SicNlhI+eoSyNjSDq9QoGUSR+mKtsZT9UipaNxsJM0/zYZ4YjtTRcvxShY</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">/7GJp+P0jPdkDNjpq7jwwFOiVB1E/T+5v24ZQMaOca8zKh/L/XOGnMVBs+bPO4/H</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">jXDX2iLszEDhvo8xDqqnViOLi3kzx65uKUrmVLrHuyHp0zd1sdTA+An48mWWSFke</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">l7/n+zCdAqW76WexODI3py32/IP7qZdi9ZL9y8/1FGy+J9/AwmNdd8JCWOcek+eH</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">JKh1JoYq7C9kgh2+9VQLcVIRAcgkwXTxDdfB4fkDiik6eK5iWIza75uBOcsCAwEA</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">AQKCAgA3hxdEd4C6ZFHVBcRxLXmNkK8gRr0XdjlrLzvS8I07KnL6vYm0+LRWRvV3</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Bi8IymOgz9ybJJiNHV2Otsp2xl5mmBRk916/dVRFWOXw+G3I6Y9hom4GPS+XVoB3</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">22CsHH/EXFjt/G+/+WI/7IlcdlWJjeylhaLqRDQCwxvQ1kB4SEMy6WBhTyeoYZUs</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">aK6RVI+IuCCiV4JKxEcaLyErv5iMpCAt/HFe/CrPsuD6/YJzOj+4CVc5RSiBEIP9</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">1ZzHUPsekYXdaQ/cMs4bUegTxKsCB/EhlSGa6Nx1Snv9gBKIKy0ZT8GcomoJIPCp</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dXxcPUZFBDbqj3aVPHEEYAmxbu3QFuJDN216BMD5rHgMXfWrGZ/UvTx0+5HmVTiN</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">qhPWUNdkMWUVNqVPNCbHpSpK0ip0vknxqs/xf17GHLKhHh4RJ/gbEnhQOU0E6qIG</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Wke0aM6mHnhuYFbHc6Ux1Ow2S0RZ1pC6kTJC4vpnGGZekdv033MTLNly/8uK6j7x</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">eTQRxEi3cl8aMLRKFYkn2F8sE4ZIq6x7Q2knCaHgELObJmfRDoPD2kDZ+kd2AT9I</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">wUXokHtfEOmVQ3N8MA8rneC3DFWfs4eG6w2SzsAYYjEc06g/lzF6jjrLc2RVQNZi</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">EI3e591Kjsu56yvGoGj6PQNpv612oeQdbZQem99BAz128xzijQKCAQEA9SuAAyTA</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">2JVCieUYybtjpJ9BNxHchXfqkB9CHOwYCbocjgkfSFzUX5NNkpIqUpaahQYE6+B4</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">zFopyPkzUObdUWGTKmMbnlvkE3d/mgeliDCHMGN/BgmtvnIPfHM72zQHeXwJIEp/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">p+u7EEr0JdiKHORyAQ3CP2TWA7aKstPSj0lNF9ov68TlWeYmzHZ35W1eAHmuhkA4</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">FyhjggUaqf9RD4dO5mJJgbkEn4qIdZjG1KHunOMuFUuotG0BxN1W9ozOFrYqkIYo</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">kh8E5zLUFP2sLA9K2imVDyOBlbMRjCaTx/QWOJpmPdRuhUQPeuQuaD5yZ/DoC4Nj</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">i/00WdwT09OJFwKCAQEA5dQj6cz5oAUA761STeesmjIQx2UeIobPCsIsDjIaqqUn</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">CdT3pKHxstS9tcE77e6vctTGu70IMMJcqVcP7M1p4WyhuqWW4QvmDeXWpfSZCoTm</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">3CPMQe1JgBCxk2Ysv8vCXTxh+pPYRevLJo2LdpxdQhBXVdgq2QdkroAcKNI5kUwy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">B8HAl/AaIwAInYt0cRJtdxxm1VU6bwfAAtvDdHnf6ozSAsj/qzSKZufR0zGXdHj+</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">d6kuhL3h6XDC8vaDMkhIq4/2vc8Q+Q+uIt89ulXZnm/ecZAaz2I3pRdiYGpK3rJI</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">2hSWMa8CwbAO/s0rMi3cOdVtzyB4NQA6YzBfXAHdbQKCAQEAiqd3Vk/K4LJJV0NB</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">qZVY3RlUAJkRZkpTVf7XnRPI5Hdk/s8Z6grV8NeV33baiKqz6NdmoR/qMWgsiYSm</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">CPMH56aPXEGAtSay7m9+stXwP/1x92PNytRedn57U564E1hxgbssTh7T41vmB2jc</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">eyrjsUNhVkc5vDCC+Mab2lcBlheWig7TWzuXmigXeTrmw9H+f7ZRhYt8LVyiKj5I</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">viuu5GFZteGQQNUqUDBAzIECse6CzCePQGvxlViwz21+aSFozF9p4yJ8f2cblsWg</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">O97bKG/dnVQ3AMDL4KXNHr4Shdr9VjW77OvfNW8wcgfqVvo6KZ6NHTXJbrpciRV2</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">AS55MQKCAQAgn8+FRcJ9k5hBbZIMWwGmVcsJ8vGnJwnG1i9YyIX+iTkUbTXGGfLs</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">VsSjQ03VhZdELq4TQk8iVMTAiQpKrBIGnNwMWaskQQMkH+crYaBD3P/EahR5m0xe</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sH/0k4FwrGTnxUWHQ2tk6YbJ7zfH24Rld1peZjQW4LTLDIZyyPzBAlbrwPYs2zlq</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">lHukmYIq9tmxGcgDKnkD/86i2T1SkGZXriu1l4CrdebbmravSw/byb0dkVd4Ilgq</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">QgZ6ZMHU5mviZqo44WbgY/htHNFvXPZqRikGZ9VWcPPDLKGbXBtKp3Yt9tQnM1ep</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">1OscAlXKZrYnQ4O17j8CDwk6wUgYEG+1AoIBAQCqCU6FMEahhDBJgTGds4zS5DLp</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">MFrfHtmo4YwPwk/Q/rt0/Ipdp0WRwcHNtYBHluYB22PaCwQd+k+3KqBDwqSPtLhz</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">2qsOgdDiRVsBL/UoCOERMc+YittrgOISYMqKvgi4FL5taK0+Lsy3i9JsMU6wPxy0</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">mMorxklqVarCYYr/ydCpp8Ty48NzYQHyUPIS/b2jdIqBIUMf2OCmw5vvDn4QYmiA</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Kt5ed/7+WQ+2GT9/0FuHrFMkHFRSKTG9ac1B3NyfLBKJ7ZbedMx0MTlmwmwgW79f</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Kj6Wklu6G8M0umKPTs1wPOy/xCtafMeMRf8tVoyfUyhxofkEpkbp1eUFAEhI</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">-----END RSA PRIVATE KEY-----</span><br></span></code></pre></div></div>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><ul>
<li class=""><strong>ALY_USER:</strong> 服务器配置的部署用户名</li>
<li class=""><strong>ALY_HOST:</strong> 服务器域名</li>
<li class=""><strong>ALY_SSH_TOKEN:</strong> 通过<code>ssh-keygen</code> 生成的私钥</li>
</ul></div></div>
<h4 class="anchor anchorTargetStickyNavbar_dDPi" id="项目-workflow-配置">项目 workflow 配置<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E9%A1%B9%E7%9B%AE-workflow-%E9%85%8D%E7%BD%AE" class="hash-link" aria-label="项目 workflow 配置的直接链接" title="项目 workflow 配置的直接链接" translate="no">​</a></h4>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><p>配置文件配置在项目中，通常写在 <code>.github/workflows</code> 目录下</p></div></div>
<p><strong>example:</strong></p>
<div class="language-yml codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_X2iu">.github/workflows/deploy.yml</div><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-yml codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Deployment</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 触发脚本的条件，develop分支push代码的时候</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">on</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">push</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">branches</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> release</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 要执行的任务</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">jobs</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># 任务名称</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">build</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># runs-on 指定job任务运行所需要的虚拟机环境（必填）</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># runs-on: self-hosted</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">runs-on</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ubuntu</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 任务步骤</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">steps</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic"># 获取源码</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> pull code</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># 使用action库  actions/checkout获取源码</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">uses</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> actions/checkout@v3</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic"># 安装node</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> create build environments</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># 使用action库 actions/setup-node 安装node</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">uses</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> actions/setup</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">node@v3</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">with</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">node-version</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 18.20.4</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic"># 安装依赖</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> install</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">run</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> npm install</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic"># 打包</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> build</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">run</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> npm run build</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Upload static files as artifact</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">id</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> deployment</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">uses</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> actions/upload</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">pages</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">artifact@v3 </span><span class="token comment" style="color:#999988;font-style:italic"># or specific "vX.X.X" version tag for this action</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">with</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">path</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> build/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic"># 上传打包文件到远程服务器</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ssh deploy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">uses</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> easingthemes/ssh</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">deploy@v2.2.11</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">env</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">REMOTE_USER</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">secrets.ALY_USER</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">REMOTE_HOST</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">secrets.ALY_HOST</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">REMOTE_PORT</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"22"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">ARGS</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"-rltgoDzvO --delete"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">SSH_PRIVATE_KEY</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">secrets.ALY_SSH_TOKEN</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token comment" style="color:#999988;font-style:italic"># 要上传文件所在目录</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">SOURCE</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"build/*"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token comment" style="color:#999988;font-style:italic"># 远程服务器目标路径</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token key atrule" style="color:#00a4db">TARGET</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/var/www/timeline"</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="补充">补充<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E8%A1%A5%E5%85%85" class="hash-link" aria-label="补充的直接链接" title="补充的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="nginx-配置">NGINX 配置<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#nginx-%E9%85%8D%E7%BD%AE" class="hash-link" aria-label="NGINX 配置的直接链接" title="NGINX 配置的直接链接" translate="no">​</a></h3>
<p>以访问 <code>http://blog.biulight.cn/timeline</code> 能显示 <code>/var/www/timeline</code> 目录内容为例</p>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><ol>
<li class="">通常，Nginx 的虚拟主机配置位于<code>/etc/nginx/sites-available/</code>目录下。如果已经有针对<code>timeline</code>的配置文件，可以直接编辑它；否则，创建一个新的配置文件。</li>
<li class="">启用配置文件，需要在 <code>/etc/nginx/sites-enabled/</code>目录下有对应配置文件的映射。</li>
</ol></div></div>
<ol start="3">
<li class="">新增配置文件</li>
</ol>
<div class="language-nginx codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_X2iu">/etc/nginx/sites-available/timeline</div><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-nginx codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">server {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    listen 80;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    server_name blog.biulight.cn;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    # 映射/timeline到指定目录</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    location /timeline {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        alias /var/www/timeline;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        index index.html index.htm;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre></div></div>
<ol start="2">
<li class="">启用配置文件</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain"># 建立软连接，启用新增的配置文件</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ln -s /etc/nginx/sites-available/timeline /etc/nginx/sites-enabled/timeline</span><br></span></code></pre></div></div>
<ol start="3">
<li class="">测试配置文件</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain"># 修改完配置后，运行以下命令检查语法是否正确</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sudo nginx -t</span><br></span></code></pre></div></div>
<ol start="4">
<li class="">重新加载 Nginx 服务</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain"># 如果测试通过，重新加载Nginx以应用更改：</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sudo systemctl reload nginx</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="使用-lets-encrypt-证书">使用 <code>Let's Encrypt</code> 证书<a href="https://blog.biulight.top/timeline/blog/2025/02/05/deploy-by-github#%E4%BD%BF%E7%94%A8-lets-encrypt-%E8%AF%81%E4%B9%A6" class="hash-link" aria-label="使用-lets-encrypt-证书的直接链接" title="使用-lets-encrypt-证书的直接链接" translate="no">​</a></h3>
<div class="theme-admonition theme-admonition-tip admonition_FTDX alert alert--success"><div class="admonitionHeading_WkVZ"><span class="admonitionIcon_uy2W"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_ESmc"><p>ubuntu 安装查看<a href="https://certbot.eff.org/instructions?ws=nginx&amp;os=snap" target="_blank" rel="noopener noreferrer" class="">certbot</a>官方文档</p></div></div>
<p>使用<strong>certbot</strong>自动生成证书</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo certbot --nginx</span><br></span></code></pre></div></div>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="Linux" term="Linux"/>
        <category label="Ubuntu" term="Ubuntu"/>
        <category label="Deploy" term="Deploy"/>
        <category label="GitHub" term="GitHub"/>
        <category label="CI/CD" term="CI/CD"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Install Neovim]]></title>
        <id>https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim</id>
        <link href="https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim"/>
        <updated>2025-02-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Ubuntu 怎样安装指定版本的 Neovim?]]></summary>
        <content type="html"><![CDATA[<p>Ubuntu 怎样安装指定版本的 Neovim?</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="背景">背景<a href="https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim#%E8%83%8C%E6%99%AF" class="hash-link" aria-label="背景的直接链接" title="背景的直接链接" translate="no">​</a></h2>
<p>由于 apt 中只有 Neovim(v0.72)的安装包。想使用新版需要自己安装</p>
<blockquote>
<p>以 <code>v0.9.5</code> 版本为例</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="方案一">方案一<a href="https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim#%E6%96%B9%E6%A1%88%E4%B8%80" class="hash-link" aria-label="方案一的直接链接" title="方案一的直接链接" translate="no">​</a></h2>
<ol>
<li class="">去 <a href="https://github.com/neovim/neovim/releases" target="_blank" rel="noopener noreferrer" class="">Github</a> 上查看指定版本的压缩包,并下载</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo wget https://github.com/neovim/neovim/releases/download/v0.9.5/nvim-linux64.tar.gz</span><br></span></code></pre></div></div>
<ol start="2">
<li class="">解压缩 <code>nvim-linux64</code> 至 <code>/opt</code> 目录</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo tar -C /opt -xzf nvim-linux64.tar.gz</span><br></span></code></pre></div></div>
<ol start="3">
<li class="">add this to your shell config (~/.bashrc, ~/.zshrc, ...):</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">export PATH="$PATH:/opt/nvim-linux64/bin"</span><br></span></code></pre></div></div>
<ol start="4">
<li class="">reload shell config</li>
</ol>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">source ~/.bashrc</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="方案二">方案二<a href="https://blog.biulight.top/timeline/blog/2025/02/04/ubuntu-install-neovim#%E6%96%B9%E6%A1%88%E4%BA%8C" class="hash-link" aria-label="方案二的直接链接" title="方案二的直接链接" translate="no">​</a></h2>
<p>使用 <code>snap</code> 包管理工具安装</p>
<div class="language-bash codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-bash codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token plain">sudo snap install nvim --classisc</span><br></span></code></pre></div></div>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="Linux" term="Linux"/>
        <category label="Ubuntu" term="Ubuntu"/>
        <category label="Neovim" term="Neovim"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Watch document.title]]></title>
        <id>https://blog.biulight.top/timeline/blog/2024/08/10/document.title</id>
        <link href="https://blog.biulight.top/timeline/blog/2024/08/10/document.title"/>
        <updated>2024-08-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[埋点监控 SDK ENTER 事件怎样获取到页面标题？]]></summary>
        <content type="html"><![CDATA[<p>埋点监控 SDK <code>ENTER</code> 事件怎样获取到页面标题？</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="背景">背景<a href="https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E8%83%8C%E6%99%AF" class="hash-link" aria-label="背景的直接链接" title="背景的直接链接" translate="no">​</a></h2>
<p>使用 <code>window.onload</code>、<code>hashchange</code> 事件获取 <code>document.title</code> 时，某些场景出现异常，值为空</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="分析">分析<a href="https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E5%88%86%E6%9E%90" class="hash-link" aria-label="分析的直接链接" title="分析的直接链接" translate="no">​</a></h2>
<p>单页面应用，<code>document.title</code> 存在异步设置的场景（如页面是后管配置的，需要等接口返回页面标题）</p>
<h2 class="anchor anchorTargetStickyNavbar_dDPi" id="方案">方案<a href="https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E6%96%B9%E6%A1%88" class="hash-link" aria-label="方案的直接链接" title="方案的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="场景一多页面应用title-写在-html-文件里">场景一：多页面应用，<code>title</code> 写在 html 文件里；<a href="https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E5%9C%BA%E6%99%AF%E4%B8%80%E5%A4%9A%E9%A1%B5%E9%9D%A2%E5%BA%94%E7%94%A8title-%E5%86%99%E5%9C%A8-html-%E6%96%87%E4%BB%B6%E9%87%8C" class="hash-link" aria-label="场景一多页面应用title-写在-html-文件里的直接链接" title="场景一多页面应用title-写在-html-文件里的直接链接" translate="no">​</a></h3>
<p><code>window.onload</code> 事件可以正常获取到页面标题</p>
<h3 class="anchor anchorTargetStickyNavbar_dDPi" id="场景二单页面应用title-在页面初始化后设置">场景二：单页面应用，<code>title</code> 在页面初始化后设置；<a href="https://blog.biulight.top/timeline/blog/2024/08/10/document.title#%E5%9C%BA%E6%99%AF%E4%BA%8C%E5%8D%95%E9%A1%B5%E9%9D%A2%E5%BA%94%E7%94%A8title-%E5%9C%A8%E9%A1%B5%E9%9D%A2%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%8E%E8%AE%BE%E7%BD%AE" class="hash-link" aria-label="场景二单页面应用title-在页面初始化后设置的直接链接" title="场景二单页面应用title-在页面初始化后设置的直接链接" translate="no">​</a></h3>
<blockquote>
<p>借助 <code>MutationObserver</code> 观察 <code>title</code> 元素更新</p>
</blockquote>
<div class="language-js codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-js codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">watchForTitleChanges</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">callback</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> ele </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getElementsByTagName</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"title"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">ele</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ele </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createElement</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"title"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">head</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">appendChild</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ele</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> previousTitle </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">title</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> observer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">MutationObserver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">mutationList</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> observer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> title </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">title</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">previousTitle </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      previousTitle </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      callback</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">title</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"once"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> observer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">disconnect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  observer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">observe</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ele</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">childList</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 非仅监听一次更新时，返回取消监听函数</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"once"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> observer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">disconnect</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>完美实现如下：</p>
<div class="language-js codeBlockContainer_oFpl theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_VBZe"><pre tabindex="0" class="prism-code language-js codeBlock_sDfG thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_FP2B"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 页面初次加载时，在 load 事件中注册 watch</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token dom variable" style="color:#36acaa">window</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"load"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">watchForTitleChanges</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">title</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"title发生了变化"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"once"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 路由切换时，在 hashchange 事件中注册 watch</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token dom variable" style="color:#36acaa">window</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"hashchange"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">watchForTitleChanges</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">title</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"title发生了变化"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> title</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"once"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>]]></content>
        <author>
            <name>feilx</name>
            <email>biulight007@gmail.com</email>
            <uri>https://github.com/biulight</uri>
        </author>
        <category label="Web" term="Web"/>
        <category label="Supervisory control" term="Supervisory control"/>
    </entry>
</feed>