<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Wang</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://blog.wybyte.top/</id>
  <link href="https://blog.wybyte.top/" rel="alternate"/>
  <link href="https://blog.wybyte.top/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Wang</rights>
  <subtitle>个人博客网站，分享一些基础知识和想记录的事情</subtitle>
  <title>Wang的博客</title>
  <updated>2026-09-28T02:13:28.501Z</updated>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="工具" scheme="https://blog.wybyte.top/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="Codex" scheme="https://blog.wybyte.top/tags/Codex/"/>
    <category term="ChatGPT" scheme="https://blog.wybyte.top/tags/ChatGPT/"/>
    <category term="Python" scheme="https://blog.wybyte.top/tags/Python/"/>
    <category term="SQLite" scheme="https://blog.wybyte.top/tags/SQLite/"/>
    <category term="Windows" scheme="https://blog.wybyte.top/tags/Windows/"/>
    <content>
      <![CDATA[<p>起因是一个很具体的现象：桌面端的「归档」一直提示失败，换一条对话还是失败，重启也没用。</p><p>顺着这个问题查下去，发现真正有价值的东西不在「归档坏了」本身，而在于：<strong>一条对话在本地到底被存成了多少份、分散在多少个地方</strong>。把这张地图画清楚之后，归档失败的原因、为什么应用自带的「删除」删不干净、以及一个彻底删除的工具该怎么写，就都顺理成章了。</p><p>本文分两部分：前半部分是实测出来的本地存储结构，后半部分是据此写的清理工具。</p><blockquote><p>文中的盘符、目录名、线程 ID 和对话标题均为脱敏示例，请替换成自己的实际值。</p></blockquote><h2 id="一、先确定「桌面端」是哪个程序"><a href="#一、先确定「桌面端」是哪个程序" class="headerlink" title="一、先确定「桌面端」是哪个程序"></a>一、先确定「桌面端」是哪个程序</h2><p>这一点比想象中重要。OpenAI 现在有好几个同名或近名的客户端，路径也不一样：</p><table><thead><tr><th>形态</th><th>进程名</th><th>本地数据根</th></tr></thead><tbody><tr><td>桌面客户端（ChatGPT + Codex 合一）</td><td><code>ChatGPT.exe</code>、<code>codex.exe</code></td><td><code>%USERPROFILE%\.codex</code></td></tr><tr><td>VS Code 扩展</td><td><code>codex.exe</code></td><td>同样是 <code>%USERPROFILE%\.codex</code></td></tr><tr><td>网页版</td><td>浏览器标签</td><td>不落本地会话，只有浏览器缓存</td></tr></tbody></table><p>判断方法很直接：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Get-Process</span> | <span class="built_in">Where-Object</span> &#123; <span class="variable">$_</span>.ProcessName <span class="operator">-match</span> <span class="string">&#x27;chatgpt|codex&#x27;</span> &#125; |</span><br><span class="line">  <span class="built_in">Select-Object</span> ProcessName, Id, Path</span><br></pre></td></tr></table></figure><p>如果 <code>Path</code> 落在 <code>WindowsApps</code> 下面（形如 <code>...\WindowsApps\OpenAI.Codex_&lt;版本&gt;_x64__&lt;哈希&gt;\app\ChatGPT.exe</code>），那就是打包安装的桌面客户端。</p><p><strong>它的本地数据根是 <code>%USERPROFILE%\.codex</code></strong>，不是 AppData 里的某个目录。这一点容易猜错——<code>.codex</code> 是个隐藏目录，很多人第一次找根本不会往家目录想。</p><h2 id="二、归档为什么一直失败"><a href="#二、归档为什么一直失败" class="headerlink" title="二、归档为什么一直失败"></a>二、归档为什么一直失败</h2><h3 id="归档是本地操作，不是云端操作"><a href="#归档是本地操作，不是云端操作" class="headerlink" title="归档是本地操作，不是云端操作"></a>归档是本地操作，不是云端操作</h3><p>这一点从日志能直接确认。翻客户端日志目录，归档动作打出的是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">[electron-message-handler] Archive requested conversationId=&lt;线程ID&gt; source=recent_tasks_menu</span><br><span class="line">[AppServerConnection] response_routed ... method=thread/archive ... errorCode=-32600</span><br><span class="line">error [electron-message-handler] Request failed</span><br><span class="line">  error=&#123;&quot;code&quot;:-32600,&quot;message&quot;:&quot;no rollout found for thread id &lt;线程ID&gt;&quot;&#125;</span><br><span class="line">  failureReason=rollout_not_found</span><br><span class="line">info  [electron-message-handler] Archive skipped because thread has no active rollout</span><br><span class="line">warn  [electron-message-handler] Failed to persist inactive thread archive</span><br><span class="line">  errorMessage=&quot;Inactive thread archive did not persist&quot;</span><br></pre></td></tr></table></figure><p>而整份日志里<strong>没有任何</strong> <code>backend-api/conversation</code> 之类的云端请求。也就是说：桌面端侧栏里那条对话的归档，走的是本地 <code>thread/archive</code>，没有碰云端会话。</p><h3 id="归档的动作是「把文件搬走」"><a href="#归档的动作是「把文件搬走」" class="headerlink" title="归档的动作是「把文件搬走」"></a>归档的动作是「把文件搬走」</h3><p>进一步能看出，<code>thread/archive</code> 需要把这条对话的 rollout 文件搬走：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">%USERPROFILE%\.codex\sessions\2026\09\24\rollout-2026-09-24T17-07-29-&lt;线程ID&gt;.jsonl</span><br></pre></td></tr></table></figure><p>于是失败条件就很清楚了——<strong>数据库里还有这条线程的记录，但它指向的文件已经不存在了</strong>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">state_5.sqlite → threads 表里 9 条记录</span><br><span class="line">其中 3 条：rollout_path 指向的文件不存在</span><br><span class="line">           → 归档时找不到文件 → 报 no rollout found → 界面显示「归档失败」</span><br><span class="line">其余 6 条：文件都在 → 归档正常（日志里 errorCode=null）</span><br></pre></td></tr></table></figure><p>这也解释了一个反直觉的现象：<strong>这些对话在界面上能正常打开、能看历史</strong>，但就是归档不了。原因在下一节。</p><h3 id="文件是怎么没的"><a href="#文件是怎么没的" class="headerlink" title="文件是怎么没的"></a>文件是怎么没的</h3><p>这点只能给推断，不能当结论。几条证据指向客户端的 rollout 迁移流程：</p><ul><li><code>state_5.sqlite</code> 里有 <code>rollout_migration_state</code> 表，记录 <code>legacy_to_paginated_v1</code> 迁移检查到哪条线程（<code>last_checked_thread_id</code> &#x2F; <code>last_checked_thread_created_at</code>）；</li><li>该水位线指向的线程，创建时间<strong>晚于</strong>那 3 条出问题的线程，说明它们确实在迁移的扫描范围内；</li><li>记录跳过的表 <code>rollout_migration_skipped_rollouts</code> 为空，<code>rollout-migrations/</code> 目录也是空的。</li></ul><p>具体是谁删的、为什么没同步清理数据库记录，我没有官方文档可查。但从结果看，<strong>文件被消费掉、<code>threads</code> 行留了下来</strong>。</p><blockquote><p>顺带一个实用发现：原始的 rollout 文件已经没了，但<strong>它原本多大是能算出来的</strong>。<br><code>thread_history_1.sqlite</code> 的 <code>thread_history_projection_state</code> 表里有一列<br><code>next_rollout_byte_offset</code>，记录投影已经读到原文件的第几个字节。<br>这三个值分别是 2493389 &#x2F; 93247 &#x2F; 372304 字节——这就是三个文件的原始大小。</p></blockquote><h2 id="三、完整存储地图：一条对话可能散落在-20-个地方"><a href="#三、完整存储地图：一条对话可能散落在-20-个地方" class="headerlink" title="三、完整存储地图：一条对话可能散落在 20 个地方"></a>三、完整存储地图：一条对话可能散落在 20 个地方</h2><p>这是全文最想留下来的东西。桌面端并没有把一条对话存成「一个文件」或「一张表」，而是拆得很细。下表是逐项实测出来的，其中 <code>goals</code> &#x2F; <code>queue</code> 之类的几项在多数情况下是空的，但清理时不能假设它们一定为空：</p><table><thead><tr><th>#</th><th>位置</th><th>存什么</th></tr></thead><tbody><tr><td>1</td><td><code>state_5.sqlite</code> → <code>threads</code></td><td>主记录：ID、标题、<code>rollout_path</code>、时间、归档位、项目归属</td></tr><tr><td>2</td><td><code>state_5.sqlite</code> → <code>thread_dynamic_tools</code></td><td>该对话的动态工具定义</td></tr><tr><td>3</td><td><code>state_5.sqlite</code> → <code>thread_spawn_edges</code></td><td>父子线程边（子代理）</td></tr><tr><td>4</td><td><code>state_5.sqlite</code> → <code>thread_attachments</code></td><td>附件</td></tr><tr><td>5</td><td><code>state_5.sqlite</code> → <code>rollout_migration_state</code></td><td>迁移水位线，可能指向某个线程</td></tr><tr><td>6</td><td><code>thread_history_1.sqlite</code> → <code>thread_items</code></td><td><strong>消息条目</strong>（界面显示的实际来源）</td></tr><tr><td>7</td><td><code>thread_history_1.sqlite</code> → <code>thread_turns</code></td><td>轮次元信息</td></tr><tr><td>8</td><td><code>thread_history_1.sqlite</code> → <code>thread_history_projection_state</code></td><td>投影进度（字节偏移 + 序号）</td></tr><tr><td>9</td><td><code>thread_history_1.sqlite</code> → <code>thread_realtime_items</code></td><td>实时会话条目</td></tr><tr><td>10</td><td><code>sqlite\codex-dev.db</code> → <code>local_thread_catalog</code></td><td><strong>侧栏列表的数据源</strong></td></tr><tr><td>11</td><td><code>sqlite\codex-dev.db</code> → <code>thread_timeline_ledger</code> 等</td><td>目录同步状态</td></tr><tr><td>12</td><td><code>session_index.jsonl</code></td><td>线程 ID → 标题的轻量索引</td></tr><tr><td>13</td><td><code>sessions\**\rollout-*.jsonl</code></td><td>原始会话流水（归档操作的对象）</td></tr><tr><td>14</td><td><code>.codex-global-state.json</code></td><td>客户端全局状态，<strong>十几个键家族</strong>都按线程 ID 索引</td></tr><tr><td>15</td><td><code>memories</code>*</td><td>由对话提炼的记忆与摘要</td></tr><tr><td>16</td><td><code>visualizations\&lt;年&gt;\&lt;月&gt;\&lt;日&gt;\&lt;线程ID&gt;\</code></td><td>对话产生的可视化产物</td></tr><tr><td>17</td><td><code>cap_sid</code></td><td>路径 → 沙箱 SID 的映射，含上面那些目录</td></tr><tr><td>18</td><td><code>thread-writer-locks\&lt;线程ID&gt;.lock</code></td><td>写入锁</td></tr><tr><td>19</td><td><code>goals_1.sqlite</code> &#x2F; <code>queue_1.sqlite</code></td><td>目标与队列（通常是空的）</td></tr><tr><td>20</td><td><code>logs_2.sqlite</code>、<code>.sandbox\*.log</code></td><td>日志（含正文片段）</td></tr></tbody></table><p>第 14 项值得单独展开，因为它是残留最多的地方。<code>.codex-global-state.json</code> 里按线程 ID 索引的键家族至少有这些：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">prompt-history.&lt;线程ID&gt;                          输入历史</span><br><span class="line">heartbeat-thread-permissions-by-id.&lt;线程ID&gt;      权限心跳</span><br><span class="line">thread-descriptions-v1.&lt;线程ID&gt;                  自动描述</span><br><span class="line">thread-writable-roots.&lt;线程ID&gt;                   可写根目录列表</span><br><span class="line">thread-project-assignments.&lt;线程ID&gt;              项目归属</span><br><span class="line">thread-project-membership-host-ids.&lt;线程ID&gt;      所属主机</span><br><span class="line">client-thread-bindings-v1.&lt;客户端线程ID&gt; = &lt;线程ID&gt;   绑定关系</span><br><span class="line">electron-remote-hosted-pip-task-visibility-state.&lt;线程ID&gt;</span><br><span class="line">thread-reference-capability:&lt;线程ID&gt;             顶层键，形如 &lt;前缀&gt;:&lt;ID&gt;</span><br><span class="line">thread-client-id-v1:local%3A&lt;线程ID&gt;             顶层键（ID 被 URL 编码）</span><br><span class="line">thread-tab-routes-v1:&lt;线程ID&gt;                    顶层键</span><br><span class="line">codex-writing-block-deleted-thread-v1:&lt;线程ID&gt;   删除标记</span><br><span class="line">app-server-projects-migration-by-host.*.pendingThreadAssignmentIds  ← 数组里存裸 ID</span><br></pre></td></tr></table></figure><p>注意最后一行：ID 还会以<strong>数组元素</strong>的形式出现，所以清理逻辑必须能递归处理任意嵌套的 JSON，不能只盯着键名。</p><h2 id="四、两个关键机制"><a href="#四、两个关键机制" class="headerlink" title="四、两个关键机制"></a>四、两个关键机制</h2><h3 id="1-界面能显示，靠的是数据库；能归档，靠的是文件"><a href="#1-界面能显示，靠的是数据库；能归档，靠的是文件" class="headerlink" title="1. 界面能显示，靠的是数据库；能归档，靠的是文件"></a>1. 界面能显示，靠的是数据库；能归档，靠的是文件</h3><p>这是理解整件事的钥匙。</p><p><code>thread_history_1.sqlite</code> 的 <code>thread_items</code> 是一份<strong>投影</strong>（projection）——客户端把 rollout 流水按字节逐个读进来，解析成消息条目存进 SQLite。界面渲染历史时读的是这张表，不是那个 jsonl 文件。</p><p>所以当 rollout 文件丢失后：</p><ul><li><code>thread_items</code> 里的消息还在 → <strong>界面照常显示，对话能打开</strong>；</li><li>归档要找文件搬走 → <strong>找不到，失败</strong>。</li></ul><p>两者一旦脱钩，就会出现「能看不能删」这种别扭状态。</p><h3 id="2-删除之后，正文还躺在-SQLite-的空闲页里"><a href="#2-删除之后，正文还躺在-SQLite-的空闲页里" class="headerlink" title="2. 删除之后，正文还躺在 SQLite 的空闲页里"></a>2. 删除之后，正文还躺在 SQLite 的空闲页里</h3><p>SQLite 删行只是把页标记为空闲，<strong>页里的字节不会立刻被抹掉</strong>。可以用 <code>PRAGMA freelist_count</code> 看当前有多少空闲页：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">PRAGMA freelist_count;   <span class="comment">-- 空闲页数</span></span><br><span class="line">PRAGMA page_count;       <span class="comment">-- 总页数</span></span><br></pre></td></tr></table></figure><p>只要空闲页不回收，对话正文理论上仍然可以从数据库文件里被扫描出来。所以「彻底删除」的最后一步必须是 <code>VACUUM</code>——它会重建整个数据库文件，把这些字节真正清掉。</p><p>验证方法很朴素：删完之后直接把数据库当二进制文件搜一下关键字。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">raw = <span class="built_in">open</span>(db_path, <span class="string">&quot;rb&quot;</span>).read()</span><br><span class="line"><span class="built_in">print</span>(raw.count(<span class="string">b&quot;&lt;关键字&gt;&quot;</span>))   <span class="comment"># VACUUM 之后应为 0</span></span><br></pre></td></tr></table></figure><h2 id="五、应用自带的删除为什么不彻底"><a href="#五、应用自带的删除为什么不彻底" class="headerlink" title="五、应用自带的删除为什么不彻底"></a>五、应用自带的删除为什么不彻底</h2><p>实测方式很直接：客户端里早就删掉的旧对话，拿它的 ID 回全局状态里搜，仍然能搜到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">prompt-history.&lt;已删除的线程ID&gt;                    ← 还在</span><br><span class="line">heartbeat-thread-permissions-by-id.&lt;已删除的线程ID&gt; ← 还在</span><br><span class="line">thread-reference-capability:&lt;已删除的线程ID&gt;        ← 还在</span><br><span class="line">codex-writing-block-deleted-thread-v1:&lt;已删除的线程ID&gt; ← 还在</span><br></pre></td></tr></table></figure><p>（有意思的是 <code>thread-descriptions-v1</code> 那条确实被清掉了，说明自带删除清了一部分键，但没清全。）</p><p>这就意味着：<strong>如果你在意的是「这条对话在我机器上不要留下任何痕迹」，自带删除是不够的。</strong></p><h2 id="六、工具设计"><a href="#六、工具设计" class="headerlink" title="六、工具设计"></a>六、工具设计</h2><p>需求很明确：给定一条或多条对话，把上面那张地图里的痕迹一次性清干净。工具叫 <code>codex-chat-cleaner</code>，单文件 Python，<strong>只用标准库</strong>。</p><h3 id="1-先找-rollout，再确定要删哪些-ID"><a href="#1-先找-rollout，再确定要删哪些-ID" class="headerlink" title="1. 先找 rollout，再确定要删哪些 ID"></a>1. 先找 rollout，再确定要删哪些 ID</h3><p>这是设计里最容易踩坑的地方。</p><p>一条对话可能派生出「子代理」线程（例如自动审查用的 guardian）。它们的 rollout 文件长这样：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;session_meta&quot;</span><span class="punctuation">,</span><span class="attr">&quot;payload&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;session_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;&lt;父线程ID&gt;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="string">&quot;&lt;子线程自己的ID&gt;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;parent_thread_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;&lt;父线程ID&gt;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;thread_source&quot;</span><span class="punctuation">:</span><span class="string">&quot;guardian_review&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>也就是说，<strong>子代理在 <code>threads</code> 表里是一条独立的行</strong>。</p><p>如果只删父对话、却把子代理的 rollout 文件也一起删掉，就会制造出两个新的「有记录没文件」的孤儿——<strong>正好复现本文开头那个归档失败的成因</strong>。</p><p>所以流程必须是：先扫 rollout 找出子代理 ID，把它们<strong>加入删除集合</strong>，再统一删数据库。而不是反过来。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">find_rollouts</span>(<span class="params">codex_home, ids, want_children</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;返回 (本线程 rollout, 子代理 rollout, 子代理自己的线程 id)。&quot;&quot;&quot;</span></span><br><span class="line">    own, children, child_ids = <span class="built_in">set</span>(), <span class="built_in">set</span>(), <span class="built_in">set</span>()</span><br><span class="line">    <span class="keyword">for</span> root <span class="keyword">in</span> (sessions_dir, archived_sessions_dir):</span><br><span class="line">        <span class="keyword">for</span> dirpath, _dirnames, filenames <span class="keyword">in</span> os.walk(root):</span><br><span class="line">            <span class="keyword">for</span> fn <span class="keyword">in</span> filenames:</span><br><span class="line">                <span class="keyword">if</span> <span class="keyword">not</span> fn.endswith(<span class="string">&quot;.jsonl&quot;</span>):</span><br><span class="line">                    <span class="keyword">continue</span></span><br><span class="line">                sid, oid, parent = read_session_meta(os.path.join(dirpath, fn))</span><br><span class="line">                <span class="keyword">if</span> oid <span class="keyword">in</span> ids:</span><br><span class="line">                    own.add(...)</span><br><span class="line">                <span class="keyword">elif</span> sid <span class="keyword">in</span> ids <span class="keyword">or</span> parent <span class="keyword">in</span> ids:</span><br><span class="line">                    <span class="keyword">if</span> want_children:</span><br><span class="line">                        children.add(...)</span><br><span class="line">                        <span class="keyword">if</span> oid:</span><br><span class="line">                            child_ids.add(oid)      <span class="comment"># ← 关键：把子线程也纳入删除范围</span></span><br><span class="line">    <span class="keyword">return</span> own, children, child_ids</span><br></pre></td></tr></table></figure><h3 id="2-迁移水位线要回退，不能留悬空引用"><a href="#2-迁移水位线要回退，不能留悬空引用" class="headerlink" title="2. 迁移水位线要回退，不能留悬空引用"></a>2. 迁移水位线要回退，不能留悬空引用</h3><p>如果被删线程恰好是 <code>rollout_migration_state</code> 指向的那一条，水位线就指向了一个不存在的线程。工具会把它回退到「创建时间不晚于原水位线、且仍然存活」的最近一条；一条都不剩就置空。</p><p>这条纯粹是为了不留悬空引用——<strong>语义是我从表结构和实测行为推断的，不是官方文档</strong>，所以实现上只做「不指向被删线程」这一点，不改变迁移本身的其它行为。</p><h3 id="3-删除范围做成开关"><a href="#3-删除范围做成开关" class="headerlink" title="3. 删除范围做成开关"></a>3. 删除范围做成开关</h3><p>不是所有痕迹都同等重要，所以范围是可配的：</p><table><thead><tr><th>范围</th><th>默认</th><th>内容</th></tr></thead><tbody><tr><td>核心记录</td><td>锁定</td><td>线程行 &#x2F; 消息历史 &#x2F; rollout 文件 &#x2F; 列表索引</td></tr><tr><td>全局状态残留</td><td>开</td><td><code>.codex-global-state.json</code> 的十几个键家族</td></tr><tr><td>记忆与摘要</td><td>开</td><td><code>memories_1.sqlite</code> + <code>memories/*.md</code> + <code>rollout_summaries/*.md</code></td></tr><tr><td>可视化 &#x2F; 权限 &#x2F; 锁</td><td>开</td><td><code>visualizations</code> 目录、<code>cap_sid</code> 条目、锁文件</td></tr><tr><td>子代理</td><td>开</td><td>该对话派生的 guardian 等</td></tr><tr><td>应用日志</td><td><strong>关</strong></td><td><code>logs_2.sqlite</code>、<code>.sandbox/*.log</code>、客户端调试日志</td></tr></tbody></table><p>日志默认关，是因为排查问题时它有用；而且日志里混着<strong>其它对话</strong>的内容，误伤成本高。</p><h3 id="4-递归清理全局状态"><a href="#4-递归清理全局状态" class="headerlink" title="4. 递归清理全局状态"></a>4. 递归清理全局状态</h3><p>全局状态的形状不固定——键名里可能有 ID、值可能是 ID、数组元素可能是 ID、值里的路径也可能含 ID。所以清理函数必须递归处理所有形态：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">purge_json</span>(<span class="params">node, ids, stats</span>):</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">isinstance</span>(node, <span class="built_in">dict</span>):</span><br><span class="line">        out = &#123;&#125;</span><br><span class="line">        <span class="keyword">for</span> k, v <span class="keyword">in</span> node.items():</span><br><span class="line">            <span class="keyword">if</span> <span class="built_in">isinstance</span>(k, <span class="built_in">str</span>) <span class="keyword">and</span> hit(k, ids):      <span class="comment"># 键名含 ID</span></span><br><span class="line">                stats[<span class="string">&quot;keys&quot;</span>] += <span class="number">1</span></span><br><span class="line">                <span class="keyword">continue</span></span><br><span class="line">            <span class="keyword">if</span> <span class="built_in">isinstance</span>(v, <span class="built_in">str</span>) <span class="keyword">and</span> hit(v, ids):      <span class="comment"># 值就是 ID</span></span><br><span class="line">                stats[<span class="string">&quot;keys&quot;</span>] += <span class="number">1</span></span><br><span class="line">                <span class="keyword">continue</span></span><br><span class="line">            out[k] = purge_json(v, ids, stats)          <span class="comment"># 继续往下钻</span></span><br><span class="line">        <span class="keyword">return</span> out</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">isinstance</span>(node, <span class="built_in">list</span>):</span><br><span class="line">        res = []</span><br><span class="line">        <span class="keyword">for</span> x <span class="keyword">in</span> node:</span><br><span class="line">            <span class="keyword">if</span> <span class="built_in">isinstance</span>(x, <span class="built_in">str</span>) <span class="keyword">and</span> hit(x, ids):      <span class="comment"># 数组元素是 ID</span></span><br><span class="line">                stats[<span class="string">&quot;items&quot;</span>] += <span class="number">1</span></span><br><span class="line">                <span class="keyword">continue</span></span><br><span class="line">            res.append(purge_json(x, ids, stats))</span><br><span class="line">        <span class="keyword">return</span> res</span><br><span class="line">    <span class="keyword">return</span> node</span><br></pre></td></tr></table></figure><p>一个刻意的取舍：<strong>数组被清空时保留空数组，不连键一起删</strong>。因为不知道客户端是否会因为缺键而报错，留个空数组更稳。</p><h3 id="5-记忆文件按「块」回收，不是按行"><a href="#5-记忆文件按「块」回收，不是按行" class="headerlink" title="5. 记忆文件按「块」回收，不是按行"></a>5. 记忆文件按「块」回收，不是按行</h3><p><code>memories/MEMORY.md</code> 是有层级的：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># Task Group: &lt;项目&gt;</span></span><br><span class="line">scope: ...</span><br><span class="line">applies<span class="emphasis">_to: ...</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">## Task 1: &lt;任务标题&gt;</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">### rollout_</span>summary<span class="emphasis">_files</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">- rollout_</span>summaries/&lt;文件&gt;.md (..., thread<span class="emphasis">_id=&lt;线程ID&gt;, ...)</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">### keywords</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">- &lt;关键词&gt;...</span></span><br></pre></td></tr></table></figure><p>只删那行 <code>thread_id=</code> 是不够的——<code>### keywords</code> 里的关键词、以及 <code>## User preferences</code>、<code>## Reusable knowledge</code> 这些小节的正文，<strong>都还是从这条对话提炼出来的</strong>。</p><p>所以回收逻辑是按层级判断：</p><ol><li><code>## </code> 小节里 <code>rollout_summaries/</code> 条目若全部指向被删线程 → 整个小节删掉；</li><li>其余仍含被删 ID 的行 → 逐行删掉；</li><li><code># Task Group</code> 块清理后<strong>不再剩任何 <code>## Task </code></strong> → 整块删掉（此时它的偏好、知识、失败记录等小节，都只是已消失 Task 的附属内容）。</li></ol><p>同理，<code>raw_memories.md</code> 是按 <code>## Thread `&lt;线程ID&gt;`</code> 分块的，整块删除即可。</p><h3 id="6-安全措施"><a href="#6-安全措施" class="headerlink" title="6. 安全措施"></a>6. 安全措施</h3><ul><li><strong>检测进程</strong>：<code>ChatGPT.exe</code> &#x2F; <code>codex.exe</code> 在跑就拒绝执行。客户端会把全局状态从内存整体重写，开着改等于白改。</li><li><strong>先备份再动手</strong>：所有受影响的文件复制到 <code>backups\&lt;UTC时间戳&gt;\</code>，包含原库（连 <code>-wal</code> &#x2F; <code>-shm</code>）、被删文件的副本、线程原始行快照（<code>thread_snapshot.json</code>）、以及路径清单（<code>manifest.json</code>）。</li><li><strong>事务 + 原子写</strong>：数据库改动包在 <code>BEGIN IMMEDIATE</code> 里，出错回滚；JSON 用「临时文件 + <code>os.replace</code>」写，不会留半截文件。</li><li><strong>提交后 VACUUM</strong>：清掉空闲页里的正文残留。</li><li><strong>默认干跑</strong>：先列出将执行的每一条（哪张表删几行、删哪些文件、全局状态移除几个键），要手输 <code>DELETE</code> 才真正执行。</li></ul><h2 id="七、使用"><a href="#七、使用" class="headerlink" title="七、使用"></a>七、使用</h2><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> D:\tools\codex<span class="literal">-chat-cleaner</span></span><br><span class="line"></span><br><span class="line">python cleaner.py <span class="literal">--list</span>          <span class="comment"># 列出所有对话（含 rollout 是否缺失）</span></span><br><span class="line">python cleaner.py                 <span class="comment"># 交互式 TUI</span></span><br><span class="line">python cleaner.py <span class="literal">--scan</span> <span class="literal">--id</span> &lt;线程ID&gt;        <span class="comment"># 残留自检</span></span><br><span class="line">python cleaner.py <span class="literal">--id</span> &lt;线程ID&gt; <span class="literal">--dry-run</span>     <span class="comment"># 只看计划</span></span><br><span class="line">python cleaner.py <span class="literal">--id</span> &lt;线程ID&gt;               <span class="comment"># 执行（要手输 DELETE 确认）</span></span><br></pre></td></tr></table></figure><p>TUI 的按键：</p><table><thead><tr><th>键</th><th>作用</th></tr></thead><tbody><tr><td><code>↑</code> <code>↓</code></td><td>移动光标</td></tr><tr><td><code>Space</code></td><td>勾选 &#x2F; 取消</td></tr><tr><td><code>a</code></td><td>全选 &#x2F; 全不选</td></tr><tr><td><code>Enter</code></td><td>进入预览确认页</td></tr><tr><td><code>s</code></td><td>删除范围设置</td></tr><tr><td><code>r</code></td><td>残留自检</td></tr><tr><td><code>q</code> &#x2F; <code>Esc</code></td><td>退出</td></tr></tbody></table><p>列表长这样，rollout 缺失的会直接标出来——这类正是会归档失败的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Codex 对话清理器  (4 条对话)</span><br><span class="line">─────────────────────────────────────────────────────</span><br><span class="line">[ ] 11111111  示例对话 A           09-28 08:40   78条</span><br><span class="line">[x] aaaaaaaa  示例对话 B           09-24 17:07   53条  rollout缺失</span><br><span class="line">[ ] bbbbbbbb  示例对话 C           09-24 14:00    5条  rollout缺失</span><br><span class="line">[ ] cccccccc  Guardian review      09-24 09:45    0条</span><br><span class="line">─────────────────────────────────────────────────────</span><br><span class="line">已选 1 条</span><br><span class="line">↑↓ 移动   Space 勾选   a 全选/取消   Enter 预览   s 范围设置   r 残留自检   q 退出</span><br></pre></td></tr></table></figure><h3 id="残留自检怎么读"><a href="#残留自检怎么读" class="headerlink" title="残留自检怎么读"></a>残留自检怎么读</h3><p>删完会自动全盘再扫一遍。有两类命中是<strong>正常的</strong>，工具会标注出来：</p><ol><li><code>其他对话正文提及 &lt;ID&gt;（正常，未改动）</code>——别的对话里提到过这个 ID。那段正文属于别的对话，不该动；</li><li><code>logs_2.sqlite [logs.thread_id]</code>——日志行，默认保留。</li></ol><p>真正要看的是输出的「其中需关注 N 处」，正常情况下应为 <strong>0</strong>。</p><h2 id="八、实测结果"><a href="#八、实测结果" class="headerlink" title="八、实测结果"></a>八、实测结果</h2><p>拿三条「有记录、没文件」的对话跑完整流程：</p><table><thead><tr><th>项目</th><th>前</th><th>后</th></tr></thead><tbody><tr><td><code>threads</code></td><td>9</td><td>4（三条目标 + 两个子代理一并清除）</td></tr><tr><td>迁移水位线</td><td>指向被删的子代理线程</td><td>回退到存活线程</td></tr><tr><td><code>thread_items</code></td><td>247</td><td>78</td></tr><tr><td><code>thread_turns</code></td><td>7</td><td>1</td></tr><tr><td><code>thread_history_projection_state</code></td><td>8</td><td>3</td></tr><tr><td><code>local_thread_catalog</code>（侧栏数据源）</td><td>4</td><td>1</td></tr><tr><td><code>stage1_outputs</code></td><td>1</td><td>0</td></tr><tr><td><code>.codex-global-state.json</code></td><td>31 个键 + 3 个数组项命中</td><td>0</td></tr><tr><td><code>MEMORY.md</code></td><td>整块来自被删对话</td><td>空</td></tr><tr><td><code>session_index.jsonl</code></td><td>4 条</td><td>1 条</td></tr><tr><td>SQLite <code>freelist_count</code></td><td>—</td><td>0（VACUUM 生效）</td></tr></tbody></table><p>删完后按二进制搜数据库文件，<code>state_5.sqlite</code>、<code>codex-dev.db</code>、<code>memories_1.sqlite</code>、<code>session_index.jsonl</code>、<code>cap_sid</code> 全部为 0 命中。</p><p>唯一「残留」的是 <code>thread_history_1.sqlite</code> 里的一批字节——追下去发现它们全部属于<strong>另一条仍然存活的对话</strong>的消息正文（那条对话的正文里提到了这些 ID）。这类命中是正常的，工具的自检会把它标成「其他对话正文提及」。</p><h2 id="九、边界、风险与恢复"><a href="#九、边界、风险与恢复" class="headerlink" title="九、边界、风险与恢复"></a>九、边界、风险与恢复</h2><p><strong>这个工具不能做到的：</strong></p><ul><li>不恢复已经丢失的 rollout 内容。归档失败那几条的文件是真没了，删除只是把残缺记录一并清掉；</li><li>不动<strong>云端</strong>会话。本文全程是本地数据，网页端账号里的对话不受影响；</li><li>不修客户端自身的 bug。它清的是残留，不是修行为。</li></ul><p><strong>风险与恢复：</strong></p><p>删除不可逆（除备份外）。出问题时：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 先退出客户端</span></span><br><span class="line"><span class="comment"># 2. 把 orig/ 里的文件按原名覆盖回去</span></span><br><span class="line"><span class="built_in">Copy-Item</span> <span class="string">&quot;backups\&lt;时间戳&gt;\orig\state_5.sqlite&quot;</span> <span class="string">&quot;<span class="variable">$env:USERPROFILE</span>\.codex\state_5.sqlite&quot;</span> <span class="literal">-Force</span></span><br><span class="line"><span class="comment"># -wal / -shm 若有备份一并覆盖</span></span><br><span class="line"><span class="comment"># 3. 按 manifest.json 里的 removed_files[].path 把 files/ 里的文件放回原路径</span></span><br></pre></td></tr></table></figure><p>备份里的 <code>thread_snapshot.json</code> 保存了被删线程在 <code>threads</code> &#x2F; <code>thread_items</code> &#x2F; <code>thread_turns</code> 里的完整原始行，必要时可以手工 <code>INSERT</code> 回去。</p><p><strong>一个已知限制：</strong> 子代理 ID 只能从 rollout 文件里反查。如果先删了一次（当时没开日志范围），之后想再清日志里属于子代理的那些行，需要手动把子代理 ID 也传进去：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">python cleaner.py <span class="literal">--logs</span> <span class="literal">--yes</span> `</span><br><span class="line">  <span class="literal">--id</span> &lt;父线程ID&gt; <span class="literal">--id</span> &lt;子代理ID&gt; <span class="literal">--id</span> &lt;另一个子代理ID&gt;</span><br></pre></td></tr></table></figure><h2 id="十、下载"><a href="#十、下载" class="headerlink" title="十、下载"></a>十、下载</h2><ul><li>脚本：<a href="https://imgs.wybyte.top/file/codex-chat-cleaner/cleaner.py">https://imgs.wybyte.top/file/codex-chat-cleaner/cleaner.py</a></li><li>说明文档：<a href="https://imgs.wybyte.top/file/codex-chat-cleaner/README.md">https://imgs.wybyte.top/file/codex-chat-cleaner/README.md</a></li></ul><p>README 里记录了完整的落点清单和恢复步骤。</p><h2 id="附录：几个值得留意的实现细节"><a href="#附录：几个值得留意的实现细节" class="headerlink" title="附录：几个值得留意的实现细节"></a>附录：几个值得留意的实现细节</h2><p><strong>1. 删库时顺手 VACUUM 会拖慢速度，但省不掉。</strong> 只在被改动的小库上做（几百 KB 到几 MB），30MB 的日志库单独给开关。</p><p><strong>2. TUI 的宽度计算要按东亚字符算。</strong> 中文标题按 1 个字符算宽度会导致整列错位：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">char_w</span>(<span class="params">ch</span>):</span><br><span class="line">    <span class="keyword">return</span> <span class="number">2</span> <span class="keyword">if</span> unicodedata.east_asian_width(ch) <span class="keyword">in</span> (<span class="string">&quot;W&quot;</span>, <span class="string">&quot;F&quot;</span>) <span class="keyword">else</span> <span class="number">1</span></span><br></pre></td></tr></table></figure><p>按键用标准库 <code>msvcrt.getwch()</code>，方向键是 <code>\x00</code> &#x2F; <code>\xe0</code> 前缀加一个字母，不需要任何第三方依赖。</p><p><strong>3. 描述和预览共用同一份「计划对象」。</strong> 先构造出 <code>Plan</code>（里面是一串 <code>Op</code>），预览页遍历它打印，执行页遍历它运行。这样<strong>预览看到的就是真正会执行的</strong>，不会出现两边逻辑漂移。</p><p><strong>4. 扫残留时要区分「该删的」和「别人正文里提到的」。</strong> 后者如果一并删掉，就等于改了另一条对话的历史，所以自检里必须把两类分开报。</p><h2 id="相关阅读"><a href="#相关阅读" class="headerlink" title="相关阅读"></a>相关阅读</h2><ul><li><a href="/2026/08/21/%E6%B8%85%E7%90%86Codex%E5%AF%B9%E8%AF%9D%E6%AE%8B%E7%95%99%E5%A4%B1%E6%95%88%E7%B4%A2%E5%BC%95/">清理 Codex 对话残留失效索引：解决侧边栏打不开或反复报错</a> —— 处理的是「孤儿索引」，本文的工具是在它的基础上把范围扩到了全局状态、记忆、日志等全部落点。</li></ul>]]>
    </content>
    <id>https://blog.wybyte.top/2026/09/28/Codex%E6%A1%8C%E9%9D%A2%E7%AB%AF%E5%AF%B9%E8%AF%9D%E5%AD%98%E5%82%A8%E5%85%A8%E8%A7%A3%E6%9E%90%E4%B8%8E%E5%BD%BB%E5%BA%95%E5%88%A0%E9%99%A4%E5%B7%A5%E5%85%B7/</id>
    <link href="https://blog.wybyte.top/2026/09/28/Codex%E6%A1%8C%E9%9D%A2%E7%AB%AF%E5%AF%B9%E8%AF%9D%E5%AD%98%E5%82%A8%E5%85%A8%E8%A7%A3%E6%9E%90%E4%B8%8E%E5%BD%BB%E5%BA%95%E5%88%A0%E9%99%A4%E5%B7%A5%E5%85%B7/"/>
    <published>2026-09-28T10:00:00.000Z</published>
    <summary>
      <![CDATA[<p>起因是一个很具体的现象：桌面端的「归档」一直提示失败，换一条对话还是失败，重启也没用。</p>
<p>顺着这个问题查下去，发现真正有价值的东西不在「归档坏了」本身，而在于：<strong>一条对话在本地到底被存成了多少份、分散在多少个地方</strong>。把这张地图画清楚]]>
    </summary>
    <title>Codex 桌面端对话存储全解析与彻底删除工具</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="使用教程" scheme="https://blog.wybyte.top/categories/%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/"/>
    <category term="WebGPU" scheme="https://blog.wybyte.top/tags/WebGPU/"/>
    <category term="Cache Storage" scheme="https://blog.wybyte.top/tags/Cache-Storage/"/>
    <category term="博客助手" scheme="https://blog.wybyte.top/tags/%E5%8D%9A%E5%AE%A2%E5%8A%A9%E6%89%8B/"/>
    <category term="Chrome" scheme="https://blog.wybyte.top/tags/Chrome/"/>
    <content>
      <![CDATA[<h2 id="功能说明"><a href="#功能说明" class="headerlink" title="功能说明"></a>功能说明</h2><p>博客助手运行在访客自己的浏览器中。首次启用时，页面会从 Hugging Face 下载 ONNX 模型，保存到当前博客来源的 Cache Storage，然后使用 WebGPU 或 WASM 在本地生成回答。</p><p>它可以检索本站文章、流式回答中文问题，并在回答下方附上原文链接。对话问题和生成过程不需要发送到博客服务端，但浏览器仍需联网下载页面资源、文章索引和模型文件。</p><p>这是一项实验功能。第一次使用前需要确认浏览器、磁盘空间和站点存储配置，首次下载通常需要较长时间。</p><h2 id="下载量与运行模式"><a href="#下载量与运行模式" class="headerlink" title="下载量与运行模式"></a>下载量与运行模式</h2><p>当前使用的固定模型版本如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">模型：w10110/qwen3-0.6b-zh-blog-style-onnx</span><br><span class="line">版本：a41c0b941ee4ca797b1e750e683eb4ef36ea1992</span><br></pre></td></tr></table></figure><p>页面提供两种运行模式：</p><table><thead><tr><th>模式</th><th align="right">首次下载估算</th><th>浏览器要求</th><th align="right">建议自定义配额</th></tr></thead><tbody><tr><td>WebGPU</td><td align="right">约 663 MB</td><td>支持 WebGPU，显卡支持 <code>shader-f16</code></td><td align="right">至少 700 MB，建议 800 MB</td></tr><tr><td>WASM 兼容模式</td><td align="right">约 765 MB</td><td>支持 WebAssembly</td><td align="right">至少 850 MB，建议 900 MB</td></tr><tr><td>两种模式都保留</td><td align="right">约 1.4 GB</td><td>曾分别下载两套权重</td><td align="right">建议 1.6 GB 以上</td></tr></tbody></table><p>下载估算包含模型配置、分词器和权重，不包含浏览器推理运行库及当前来源原有的其他数据。Chrome 的自定义配额表示当前来源可使用的总量，不是剩余量。例如这个来源已经使用 200 MB，再设置 700 MB，只剩约 500 MB 可用。</p><p>WebGPU 速度通常更好，页面会优先检测它。若 WebGPU 初始化失败，页面只显示原因，不会立即下载 WASM 权重；确认后再点击“切换兼容模式”，可以避免无意中连续下载两套模型。</p><h2 id="首次使用前的浏览器设置"><a href="#首次使用前的浏览器设置" class="headerlink" title="首次使用前的浏览器设置"></a>首次使用前的浏览器设置</h2><p>推荐使用最新版桌面版 Chrome 普通窗口。无痕窗口、访客模式和退出时自动删除站点数据的配置不适合保存数百 MB 的模型文件。</p><h3 id="允许站点保留本地数据"><a href="#允许站点保留本地数据" class="headerlink" title="允许站点保留本地数据"></a>允许站点保留本地数据</h3><p>打开 Chrome 设置，进入“隐私和安全”相关的站点数据设置，检查以下内容：</p><ul><li>允许当前博客来源在设备上保存数据；</li><li>当前域名不在“关闭所有窗口时删除站点数据”的列表中；</li><li>系统磁盘有足够空间，至少为准备下载的模式预留 1 GB；</li><li>本地调试和线上博客分别检查，因为它们是两个独立来源。</li></ul><p>Chrome 设置页面的名称可能随版本变化。可以在设置顶部直接搜索“站点数据”或“设备上的网站数据”。</p><h3 id="配置-Simulate-custom-storage-quota"><a href="#配置-Simulate-custom-storage-quota" class="headerlink" title="配置 Simulate custom storage quota"></a>配置 <code>Simulate custom storage quota</code></h3><p>按 <code>F12</code> 打开 DevTools，依次进入 Application → Storage。在这里可以看到当前来源的已用空间和配额，也可以使用 <code>Simulate custom storage quota</code> 模拟指定额度。</p><p><img src="https://imgs.wybyte.top/file/blog-ai-1.png" alt="Chrome DevTools 中的 Simulate custom storage quota 配置"></p><p>按下面的规则设置：</p><ol><li>如果该选项已经勾选，WebGPU 模式至少填写 <code>700 MB</code>，更建议填写 <code>800 MB</code>。</li><li>准备使用 WASM 兼容模式时，建议填写 <code>900 MB</code>。</li><li>需要同时保留 WebGPU 和 WASM 两套模型时，建议填写 <code>1600 MB</code> 或更高。</li><li>若当前来源已经保存了其他数据，应把这些用量加到模型所需空间上。</li><li>如果并不需要模拟配额，可以取消勾选，让 Chrome 使用正常的站点配额策略。</li></ol><p>该选项是 DevTools 的调试覆盖项。它会让 Chrome 按填写的 MB 数执行当前来源的配额限制，但不会增加物理磁盘空间，也不能代替正常的浏览器站点数据设置。重新打开浏览器或 DevTools 后，应再次确认该选项是否仍符合预期。</p><p>如果这里只设置 281 MB，即使系统磁盘还有很多空间，约 663 MB 的 WebGPU 模型也会在写入一部分后抛出 <code>QuotaExceededError</code>。</p><h2 id="首次启用步骤"><a href="#首次启用步骤" class="headerlink" title="首次启用步骤"></a>首次启用步骤</h2><ol><li>从博客顶部菜单打开“博客助手”。</li><li>页面显示 WebGPU 可用时，点击“下载并启用”。</li><li>保持页面打开，等待网络下载、缓存写入和模型初始化全部完成。</li><li>状态显示“本地 WebGPU 已就绪”后再发送问题。</li><li>如果 WebGPU 失败，先阅读页面给出的原因，再决定是否点击“切换兼容模式”。</li></ol><p>首次加载会依次经历不同状态：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">检查运行环境</span><br><span class="line">→ 从网络下载文件</span><br><span class="line">→ 写入浏览器缓存</span><br><span class="line">→ 文件已就绪，正在初始化模型</span><br><span class="line">→ 本地模型已就绪</span><br></pre></td></tr></table></figure><p>下载进度达到 100% 不代表整个过程已经结束。大文件还需要写入 Cache Storage，ONNX Runtime 也需要创建推理会话。看到最终“已就绪”状态后才能开始对话。</p><p>如果页面提示“浏览器缓存不可用，本次仍可运行”，说明模型文件已经交给当前推理会话，但没有完整保存。刷新或下次打开时仍可能重新下载。</p><h2 id="如何确认缓存完整"><a href="#如何确认缓存完整" class="headerlink" title="如何确认缓存完整"></a>如何确认缓存完整</h2><p>在同一个博客页面打开 DevTools，进入 Application → Cache Storage → <code>transformers-cache</code>。</p><p>大权重使用 16 MiB 分块保存，完整缓存通常包含：</p><ul><li>多条以 <code>__blog_ai_chunk=</code> 结尾的 <code>.onnx_data</code> 分块；</li><li>原始 <code>.onnx_data</code> URL 对应的一条小型完整性清单；</li><li><code>config.json</code>、分词器和 ONNX 主文件等条目。</li></ul><p>只有分块、没有原始 <code>.onnx_data</code> 清单，表示上次下载或缓存写入没有完整结束。页面不会把这种状态当成可复用的完整模型。</p><p>验证缓存复用可以按以下步骤进行：</p><ol><li>首次加载到“已就绪”；</li><li>刷新同一个来源的 <code>/chat/</code> 页面；</li><li>再次点击启用；</li><li>在 Network 面板观察 <code>.onnx_data</code> 是否仍发生数百 MB 的实际传输；</li><li>页面按钮应显示“从本地缓存启用”，随后进入初始化。</li></ol><p>Network 中出现文件名并不一定代表重新下载，关键要看传输大小和页面缓存状态。若每次都有数百 MB 的实际传输，缓存没有成功复用。</p><h2 id="清理损坏或不完整的缓存"><a href="#清理损坏或不完整的缓存" class="headerlink" title="清理损坏或不完整的缓存"></a>清理损坏或不完整的缓存</h2><p>配额不足或中途关闭页面后，<code>transformers-cache</code> 中可能保留一部分分块。它们不能组成完整模型，却会继续占用当前来源空间。</p><p>需要重新开始时，可以在 Application → Cache Storage 中删除整个 <code>transformers-cache</code>，然后刷新页面重新下载。这个操作会同时删除已经完整缓存的另一种运行模式，因此执行前先确认是否确实需要全部清理。</p><p>也可以在 Application → Storage 使用“Clear site data”，但它会清除该来源的更多数据，包括本地存储和会话信息，影响范围更大。</p><h2 id="对话功能怎么用"><a href="#对话功能怎么用" class="headerlink" title="对话功能怎么用"></a>对话功能怎么用</h2><p>模型就绪后，在输入框中询问博客文章相关问题即可。页面会先检索本站文章，再把相关片段交给本地模型生成回答。</p><ul><li>回答会逐字显示；</li><li>开启思考输出后，可以展开查看对应内容；</li><li>回答下方的引用链接可以跳转到原文；</li><li>点击停止会保留已经生成的部分；</li><li>点击重试会使用该问题当时的上下文重新生成；</li><li>点击清空会删除当前标签页保存的对话记录。</li></ul><p>最近 20 条界面消息保存在 <code>sessionStorage</code>。刷新当前标签页可以恢复，关闭标签页后不保证继续存在。问题和回答不会跨浏览器同步。</p><p>本地小模型可能出现事实错误，涉及命令、版本、配置值或操作风险时，应打开回答后的原文引用再次核对。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="每次打开都重新下载-onnx-data"><a href="#每次打开都重新下载-onnx-data" class="headerlink" title="每次打开都重新下载 .onnx_data"></a>每次打开都重新下载 <code>.onnx_data</code></h3><p>依次检查：</p><ol><li>Application → Storage 中的实际配额是否大于模型和已有数据之和；</li><li><code>Simulate custom storage quota</code> 是否误设为较小值；</li><li><code>transformers-cache</code> 是否有完整清单，而不只是若干分块；</li><li>Chrome 是否在关闭窗口时删除当前站点数据；</li><li>本次访问的协议、域名和端口是否与上次一致。</li></ol><p><code>http://localhost:4000</code>、<code>http://127.0.0.1:4000</code> 和线上 HTTPS 域名是三个不同来源，缓存不能互相复用。</p><h3 id="页面显示-QuotaExceededError"><a href="#页面显示-QuotaExceededError" class="headerlink" title="页面显示 QuotaExceededError"></a>页面显示 <code>QuotaExceededError</code></h3><p>这表示 Chrome 拒绝继续向当前来源写入数据。错误中的“已保存字节”和“失败分块”可以帮助判断中断位置。</p><p>先增大或取消 DevTools 自定义配额，再检查站点数据策略和磁盘空间。若旧分块占满额度，删除不完整的 <code>transformers-cache</code> 后重新加载。</p><h3 id="页面显示约-10-GB-剩余，仍然在-281-MB-左右失败"><a href="#页面显示约-10-GB-剩余，仍然在-281-MB-左右失败" class="headerlink" title="页面显示约 10 GB 剩余，仍然在 281 MB 左右失败"></a>页面显示约 10 GB 剩余，仍然在 281 MB 左右失败</h3><p><code>navigator.storage.estimate()</code> 返回的是估算值，不是一次大文件写入的保证。Chrome 可能模糊报告值，临时或 session-only 来源也可能受到更小的实际限制。以 Application → Storage 的实际用量、缓存写入结果和 <code>QuotaExceededError</code> 为准。</p><h3 id="出现-ONNX-Runtime-节点分配警告"><a href="#出现-ONNX-Runtime-节点分配警告" class="headerlink" title="出现 ONNX Runtime 节点分配警告"></a>出现 ONNX Runtime 节点分配警告</h3><p>类似下面的日志通常是性能提示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Some nodes were not assigned to the preferred execution providers</span><br></pre></td></tr></table></figure><p>ONNX Runtime 可能主动把形状计算等节点放到 CPU。只要后面能够完成初始化和生成，不需要把这条警告当作加载失败。</p><p>若同时出现明确的 <code>ERROR_CODE</code>、数据类型错误或页面进入失败状态，应以最后一条实际错误为准。当前版本已经处理 q4f16 logits 在采样前转换为 float32 的兼容问题；部署更新后仍出现旧错误时，先重新执行 AI 构建并强制刷新脚本资源。</p><h3 id="WebGPU-不可用"><a href="#WebGPU-不可用" class="headerlink" title="WebGPU 不可用"></a>WebGPU 不可用</h3><p>页面会检查 WebGPU、显卡适配器和 <code>shader-f16</code>。不满足条件时可以使用 WASM 兼容模式，但首次需要下载约 765 MB 的另一套权重，生成速度通常也更慢。</p><p>不要在 WebGPU 权重下载一半时直接切换。先取消当前加载，确认页面回到可操作状态，再点击兼容模式。</p><h3 id="普通-HTTP-域名无法使用缓存"><a href="#普通-HTTP-域名无法使用缓存" class="headerlink" title="普通 HTTP 域名无法使用缓存"></a>普通 HTTP 域名无法使用缓存</h3><p>生产环境应通过 HTTPS 访问。<code>localhost</code> 是本地开发特例，可以使用 <code>http://localhost:4000</code>；普通 HTTP 域名不属于安全上下文，相关存储或 WebGPU 能力可能不可用。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://developer.chrome.com/docs/devtools/application">Chrome DevTools Application 面板</a></li><li><a href="https://developer.chrome.com/blog/new-in-devtools-88#simulate-storage-quota-size-in-the-storage-pane">Chrome DevTools 自定义存储配额</a></li><li><a href="https://developer.chrome.com/docs/devtools/storage/cache">Chrome DevTools 查看 Cache Storage</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Storage_API">MDN Storage API</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/StorageManager">MDN StorageManager</a></li><li><a href="https://huggingface.co/w10110/qwen3-0.6b-zh-blog-style-onnx/tree/a41c0b941ee4ca797b1e750e683eb4ef36ea1992">Hugging Face 模型文件</a></li></ul>]]>
    </content>
    <id>https://blog.wybyte.top/2026/09/16/%E5%8D%9A%E5%AE%A2%E5%8A%A9%E6%89%8B%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</id>
    <link href="https://blog.wybyte.top/2026/09/16/%E5%8D%9A%E5%AE%A2%E5%8A%A9%E6%89%8B%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/"/>
    <published>2026-09-16T02:30:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="功能说明"><a href="#功能说明" class="headerlink" title="功能说明"></a>功能说明</h2><p>博客助手运行在访客自己的浏览器中。首次启用时，页面会从 Hugging Face 下载 ONNX 模型，保存到当前博客来源的]]>
    </summary>
    <title>博客助手使用教程：浏览器配置、模型缓存与常见问题</title>
    <updated>2026-09-16T02:33:56.251Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="前端实践" scheme="https://blog.wybyte.top/categories/%E5%89%8D%E7%AB%AF%E5%AE%9E%E8%B7%B5/"/>
    <category term="Hexo" scheme="https://blog.wybyte.top/tags/Hexo/"/>
    <category term="Butterfly" scheme="https://blog.wybyte.top/tags/Butterfly/"/>
    <category term="Transformers.js" scheme="https://blog.wybyte.top/tags/Transformers-js/"/>
    <category term="WebGPU" scheme="https://blog.wybyte.top/tags/WebGPU/"/>
    <category term="ONNX" scheme="https://blog.wybyte.top/tags/ONNX/"/>
    <category term="Cache Storage" scheme="https://blog.wybyte.top/tags/Cache-Storage/"/>
    <content>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>这次给 Hexo Butterfly 博客增加了一个“博客助手”页面。它不是接入云端聊天接口，而是在访客浏览器中下载并运行 ONNX 模型，再从博客文章中检索相关段落，生成带原文引用的中文回答。</p><p>最终页面具备以下能力：</p><ul><li>从顶部菜单进入独立的 <code>/chat/</code> 页面；</li><li>只有进入对话页并主动点击启用后，才下载模型和启动 Worker；</li><li>优先使用 WebGPU，不满足条件时由访客手动切换到 WASM 兼容模式；</li><li>在浏览器中流式生成回答，可展开思考内容；</li><li>从本地 RAG 索引检索文章，并在回答后显示原文链接；</li><li>支持停止生成、重试、清空对话和本次会话恢复；</li><li>将模型文件保存到当前站点来源的 Cache Storage，后续访问优先复用。</li></ul><p>页面看起来只是一个对话框，实际实现中最难处理的部分是大文件缓存、浏览器配额、WebGPU 精度和异步状态。本文聚焦页面设计、接入、调试和验证过程。</p><h2 id="技术方案"><a href="#技术方案" class="headerlink" title="技术方案"></a>技术方案</h2><h3 id="为什么使用独立页面"><a href="#为什么使用独立页面" class="headerlink" title="为什么使用独立页面"></a>为什么使用独立页面</h3><p>最初可选方案有两个：在所有页面显示悬浮入口，或者在顶部菜单增加独立页面。最终选择独立页面，原因很直接：</p><ul><li>模型首次下载约为数百 MB，需要完整展示下载、缓存和初始化状态；</li><li>对话、思考过程和文章引用需要足够的纵向空间；</li><li>手机端悬浮窗口容易遮挡正文，也不利于处理键盘弹出后的布局；</li><li>独立 URL 方便刷新、排错和分享，同时可以关闭侧栏、评论和顶部大图。</li></ul><p>Butterfly 菜单只需要增加一项：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">menu:</span></span><br><span class="line">  <span class="string">博客助手:</span> <span class="string">/chat/</span> <span class="string">||</span> <span class="string">fas</span> <span class="string">fa-comment-dots</span></span><br></pre></td></tr></table></figure><p>对话页使用单独的页面类型，并关闭与聊天无关的主题元素：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">博客助手</span></span><br><span class="line"><span class="attr">type:</span> <span class="string">blog-ai</span></span><br><span class="line"><span class="attr">aside:</span> <span class="literal">false</span></span><br><span class="line"><span class="attr">comments:</span> <span class="literal">false</span></span><br><span class="line"><span class="attr">top_img:</span> <span class="literal">false</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="meta"></span></span><br><span class="line"><span class="string">&lt;div</span> <span class="string">id=&quot;blog-ai-page&quot;</span> <span class="string">aria-busy=&quot;true&quot;&gt;&lt;p&gt;正在准备博客助手…&lt;/p&gt;&lt;/div&gt;</span></span><br></pre></td></tr></table></figure><h3 id="延迟加载与页面生命周期"><a href="#延迟加载与页面生命周期" class="headerlink" title="延迟加载与页面生命周期"></a>延迟加载与页面生命周期</h3><p>如果把完整对话程序直接注入主题，普通文章页也会下载大体积 JavaScript。为避免影响博客原有访问体验，主题只加载约 1.5 KB 的入口脚本：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">bottom:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&lt;script</span> <span class="string">type=&quot;module&quot;</span> <span class="string">src=&quot;/ai/blog-ai-loader.js&quot;&gt;&lt;/script&gt;</span></span><br></pre></td></tr></table></figure><p>入口脚本检测到 <code>#blog-ai-page</code> 后，才继续加载对话样式和主程序。模型 Worker 则要等访客点击“下载并启用”后才创建。最终加载层次如下：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    flowchart TD    A[访问普通文章] --&gt; B[只加载轻量入口]    C[进入 &#x2F;chat&#x2F;] --&gt; B    B --&gt; D{存在 blog-ai-page}    D --&gt;|否| E[结束]    D --&gt;|是| F[加载对话样式和主程序]    F --&gt; G[访客点击下载并启用]    G --&gt; H[创建 Worker]    H --&gt; I[检查缓存或下载模型]    I --&gt; J[初始化推理会话]  </pre></div><p>Butterfly 使用 PJAX 后，页面跳转不会每次都重新载入整个文档。因此入口还要处理重复进入和离开：进入时只挂载一次，离开对话页时终止 Worker、取消当前任务并释放状态，防止旧页面的回调继续修改新页面。</p><h2 id="模型与运行模式"><a href="#模型与运行模式" class="headerlink" title="模型与运行模式"></a>模型与运行模式</h2><p>页面固定使用以下模型版本，避免仓库更新后文件结构或计算图变化导致线上行为漂移：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">w10110/qwen3-0.6b-zh-blog-style-onnx</span><br><span class="line">a41c0b941ee4ca797b1e750e683eb4ef36ea1992</span><br></pre></td></tr></table></figure><p>文件统一从下面的版本化地址获取：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://huggingface.co/w10110/qwen3-0.6b-zh-blog-style-onnx/resolve/a41c0b941ee4ca797b1e750e683eb4ef36ea1992/</span><br></pre></td></tr></table></figure><p>页面提供两种运行方式：</p><table><thead><tr><th>模式</th><th>主要权重</th><th align="right">首次下载估算</th><th>适用条件</th></tr></thead><tbody><tr><td>WebGPU</td><td><code>onnx/model_q4f16.onnx</code> 与 <code>.onnx_data</code></td><td align="right">约 663 MB</td><td>浏览器支持 WebGPU，显卡适配器支持 <code>shader-f16</code></td></tr><tr><td>WASM 兼容模式</td><td><code>onnx/model_quantized.onnx</code> 与 <code>.onnx_data</code></td><td align="right">约 765 MB</td><td>无可用 WebGPU，或 WebGPU 初始化失败</td></tr></tbody></table><p>检测顺序是 WebGPU API、可用适配器和 <code>shader-f16</code>。WebGPU 加载失败后不会自动开始下载另一套约 765 MB 的 WASM 权重，而是显示失败原因，由访客点击“切换兼容模式”。这样可以避免一次失败触发第二次大文件下载。</p><h2 id="本地文章检索与对话状态"><a href="#本地文章检索与对话状态" class="headerlink" title="本地文章检索与对话状态"></a>本地文章检索与对话状态</h2><h3 id="构建文章索引"><a href="#构建文章索引" class="headerlink" title="构建文章索引"></a>构建文章索引</h3><p>Hexo 生成站点时会读取所有文章，移除 front matter、代码块、Hexo 标签和 Markdown 标记，再按标题与段落切分文本，生成 <code>/ai/rag-index.json</code>。每个检索单元保留文章标题、小节标题、URL 和正文。</p><p>浏览器取得问题后，先使用本地 BM25 检索最相关的文章片段，再把最多 3 条内容加入模型上下文。回答下方显示对应文章链接，访客可以回到原文核对。检索和生成都在浏览器中完成，问题不需要提交到博客服务端。</p><h3 id="控制上下文与恢复会话"><a href="#控制上下文与恢复会话" class="headerlink" title="控制上下文与恢复会话"></a>控制上下文与恢复会话</h3><p>当前配置保留最近 4 轮上下文，单次输入最多 6000 个字符，最近 20 条界面消息写入 <code>sessionStorage</code>。刷新同一个标签页可以恢复本次会话；关闭标签页或清空对话后，这些消息不会继续保留。</p><p>重试不能简单使用“当前全部消息”。生成期间用户可能停止任务，页面状态也可能变化。因此每次请求都会保存当时的问题和上下文快照，重试时使用对应快照重新发起，避免把后续消息错误地拼入旧问题。</p><h2 id="第一个坑：进度条不等于网络下载"><a href="#第一个坑：进度条不等于网络下载" class="headerlink" title="第一个坑：进度条不等于网络下载"></a>第一个坑：进度条不等于网络下载</h2><p>Transformers.js 在读取文件时会发出进度事件，但缓存命中时同样可能经过读取和解析过程。如果界面把所有进度都显示为“正在下载”，访客会误以为刷新后又下载了数百 MB。</p><p>页面后来把状态拆成四个阶段：</p><ol><li>正在从网络下载；</li><li>正在写入浏览器缓存；</li><li>文件已就绪，正在初始化模型；</li><li>模型已就绪。</li></ol><p>判断是否真正重下不能只看进度条，应同时检查 DevTools 的 Network 面板和 Application → Cache Storage。若 <code>.onnx_data</code> 每次都有数百 MB 的实际传输，而且缓存里只有配置和分词器文件，这才是缓存写入失败。</p><h2 id="第二个坑：大权重无法一次写入-Cache-Storage"><a href="#第二个坑：大权重无法一次写入-Cache-Storage" class="headerlink" title="第二个坑：大权重无法一次写入 Cache Storage"></a>第二个坑：大权重无法一次写入 Cache Storage</h2><p>最初直接把完整 <code>.onnx_data</code> 响应写入 Cache Storage。小文件可以正常保存，大权重却在写入时抛出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">QuotaExceededError: Quota exceeded.</span><br></pre></td></tr></table></figure><p>为减少单次写入压力，缓存层改为 16 MiB 分块：</p><ul><li>每个分块使用 <code>?__blog_ai_chunk=N</code> 作为本地缓存键；</li><li>下载流一边交给模型加载，一边按 16 MiB 写入 Cache Storage；</li><li>所有分块成功后，才在原始 URL 下写入一份很小的清单；</li><li>下次读取时先验证清单和全部分块，再把它们重新拼成响应流；</li><li>缺少任意分块时都不把文件标记为完整。</li></ul><p>“最后写清单”很关键。否则下载中断后，页面可能把残缺权重当成完整文件，直到 ONNX Runtime 初始化时才出现难以定位的错误。</p><p>分块只能降低单条缓存写入的压力，不能绕过浏览器对当前来源的总配额。若总配额只有约 281 MB，写到第 16 个 16 MiB 分块附近仍然会失败，这正是后续排查时遇到的现象。</p><h2 id="第三个坑：navigator-storage-estimate-不是写入保证"><a href="#第三个坑：navigator-storage-estimate-不是写入保证" class="headerlink" title="第三个坑：navigator.storage.estimate() 不是写入保证"></a>第三个坑：<code>navigator.storage.estimate()</code> 不是写入保证</h2><p>排查时，页面通过 <code>navigator.storage.estimate()</code> 得到大约 10.75 GB 总配额和 10.74 GB 剩余，看起来远大于模型文件。但 Chrome DevTools 的 Application → Storage 显示实际使用到约 281 MB 后就已满。</p><p>这两个数并不矛盾。Storage API 返回的是估算值，浏览器可能为了降低指纹识别风险而模糊结果；Chromium 对 session-only 或临时来源还有单独的额度计算和约 300 MiB 的上限。因而“估算剩余 10 GB”不能证明一次 663 MB 的 Cache Storage 写入一定成功。</p><p>实际排查应同时看三处：</p><ul><li>Application → Storage 的当前来源用量与配额；</li><li>Application → Cache Storage → <code>transformers-cache</code> 的分块数量和完整清单；</li><li>Network 面板中 <code>.onnx_data</code> 的实际传输大小。</li></ul><p>还要检查 Chrome 是否配置为关闭窗口时删除该站点数据。普通窗口也可能因此按临时站点存储处理。若 DevTools 勾选了 <code>Simulate custom storage quota</code>，Chrome 会强制执行填写的测试额度；额度小于模型所需空间时，分块缓存必然在中途失败。</p><h2 id="第四个坑：ORT-警告不是实际报错"><a href="#第四个坑：ORT-警告不是实际报错" class="headerlink" title="第四个坑：ORT 警告不是实际报错"></a>第四个坑：ORT 警告不是实际报错</h2><p>WebGPU 初始化时曾出现下面两组日志：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Some nodes were not assigned to the preferred execution providers...</span><br><span class="line"></span><br><span class="line">Unexpected input data type. Actual: (tensor(float16)), expected: (tensor(float))</span><br></pre></td></tr></table></figure><p>第一组是 ONNX Runtime 的执行提供器分配警告。形状计算等节点被放到 CPU 上不一定是故障，也不是这次加载失败的根因。真正终止运行的是第二组数据类型错误。</p><p>进一步检查两套 ONNX 图后可以确认：</p><ul><li>q4f16 模型的 KV Cache 和 logits 是 <code>float16</code>；</li><li>q8 模型的 KV Cache 和 logits 是 <code>float32</code>；</li><li>远程 <code>config.json</code> 对两种模式的 KV Cache 类型声明正确。</li></ul><p>问题发生在采样阶段。Transformers.js 3.8.1 使用的 TopK ONNX 算子要求输入为 <code>float32</code>，而 q4f16 模型输出的 logits 是 <code>float16</code>。模型加载结束后的预热会立即调用 <code>generate()</code>，所以错误看起来像“模型加载失败”，实际是在第一次采样时失败。</p><p>解决方式不是把整套 q4f16 模型改成 float32，而是在 Qwen3 的 logits processor 链最前面加入一次局部转换，只把当前 token 参与采样的 logits 转成 <code>float32</code>，再交给温度、TopK、TopP 等后续处理器。这样保留了权重和 KV Cache 的半精度优势，也覆盖预热与正常生成两个入口。</p><h2 id="第五个坑：停止任务后旧回调覆盖新状态"><a href="#第五个坑：停止任务后旧回调覆盖新状态" class="headerlink" title="第五个坑：停止任务后旧回调覆盖新状态"></a>第五个坑：停止任务后旧回调覆盖新状态</h2><p>对话页面同时包含文章检索、Worker 加载、模型生成、流式消息和 PJAX 页面切换。只给按钮加一个 <code>disabled</code> 不能完整解决并发问题。</p><p>实现中为每次加载和生成分配版本标识，并在以下场景使旧版本失效：</p><ul><li>访客取消模型加载；</li><li>停止当前生成；</li><li>从 WebGPU 切换到 WASM；</li><li>清空对话；</li><li>离开 <code>/chat/</code> 页面。</li></ul><p>Worker 回传进度、结果或错误时，必须先确认版本仍属于当前任务。停止生成时保留已经输出的文字，但终止旧 Worker，避免 WASM 仍在计算时把后续 token 混入下一轮。Worker 自身异常也要主动结束等待状态，否则界面会一直停在“正在生成”。</p><p>文章检索期间同样需要锁定提交入口。否则连续点击发送会创建两组检索与生成任务，消息顺序和引用都会错位。</p><h2 id="缓存的真实边界"><a href="#缓存的真实边界" class="headerlink" title="缓存的真实边界"></a>缓存的真实边界</h2><p>模型最终保存在浏览器的 Cache Storage 中，可在当前博客页面按 <code>F12</code> 后进入 Application → Cache Storage → <code>transformers-cache</code> 查看。它不是 Windows 上由网页指定的固定文件夹，也不会写进 Hexo 项目目录。</p><p>缓存还有几个必须接受的边界：</p><ul><li>缓存按来源隔离，协议、域名或端口任一不同都属于另一个来源；</li><li>用户清理站点数据、浏览器回收临时数据或更换浏览器后，需要重新下载；</li><li>当前分块缓存不提供 HTTP Range 断点续传；缺少最终清单时，下次仍可能重新获取完整文件；</li><li>Network 显示下载达到 100% 后，还要等待缓存写入和模型初始化完成。</li></ul><h2 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h2><p>浏览器本地模型的接入难点并不只在推理 API。一个可用的博客对话页还要同时处理资源按需加载、数百 MB 文件缓存、来源隔离、配额误差、半精度算子兼容、Worker 生命周期和异步竞态。</p><p>这次最有价值的调试经验有三点：</p><ol><li>先区分警告、表象和真正使流程停止的错误；</li><li>对大文件缓存同时观察 Network、Cache Storage 和实际配额，不能只相信一个估算值；</li><li>把“已下载”“已写入缓存”“正在初始化”“可以对话”设计成不同状态，界面才能如实反映系统正在做什么。</li></ol><p>相关资料：</p><ul><li><a href="https://developer.chrome.com/docs/devtools/application">Chrome DevTools Application 面板</a></li><li><a href="https://developer.chrome.com/docs/devtools/storage/cache">Chrome DevTools 查看 Cache Storage</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Storage_API">MDN Storage API</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/StorageManager">MDN StorageManager</a></li><li><a href="https://huggingface.co/w10110/qwen3-0.6b-zh-blog-style-onnx">Hugging Face 模型仓库</a></li></ul>]]>
    </content>
    <id>https://blog.wybyte.top/2026/09/16/Hexo-Butterfly%E5%8D%9A%E5%AE%A2%E5%8A%A9%E6%89%8B%E5%BC%80%E5%8F%91%E4%B8%8E%E8%B0%83%E8%AF%95%E5%A4%8D%E7%9B%98/</id>
    <link href="https://blog.wybyte.top/2026/09/16/Hexo-Butterfly%E5%8D%9A%E5%AE%A2%E5%8A%A9%E6%89%8B%E5%BC%80%E5%8F%91%E4%B8%8E%E8%B0%83%E8%AF%95%E5%A4%8D%E7%9B%98/"/>
    <published>2026-09-16T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>这次给 Hexo Butterfly 博客增加了一个“博客助手”页面。它不是接入云端聊天接口，而是在访客浏览器中下载并运行 ONNX 模型，]]>
    </summary>
    <title>Hexo Butterfly 博客助手开发与调试复盘：浏览器本地模型、缓存与精度问题</title>
    <updated>2026-09-16T02:37:26.101Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="运维实践" scheme="https://blog.wybyte.top/categories/%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/"/>
    <category term="Mender" scheme="https://blog.wybyte.top/tags/Mender/"/>
    <category term="OTA" scheme="https://blog.wybyte.top/tags/OTA/"/>
    <category term="Docker Compose" scheme="https://blog.wybyte.top/tags/Docker-Compose/"/>
    <category term="Kubernetes" scheme="https://blog.wybyte.top/tags/Kubernetes/"/>
    <category term="DevOps" scheme="https://blog.wybyte.top/tags/DevOps/"/>
    <content>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文记录如何使用 Mender 建立一套“总部统一发布版本，各地区服务器主动获取并升级”的系统。</p><p>目标场景如下：</p><ul><li>总部维护一个 Mender Server；</li><li>前端、后端等业务服务通过 Docker Compose 运行；</li><li>每个地区有一台或多台 Linux 服务器；</li><li>地区服务器只需要主动访问总部的 HTTPS 地址，不开放升级端口；</li><li>总部可以按地区分组、灰度发布、查看结果并停止异常批次；</li><li>发布制品必须经过签名，地区服务器拒绝未签名或被篡改的制品；</li><li>应用升级失败时能够回退容器版本；</li><li>涉及数据库变更时，由业务脚本完成备份、迁移和恢复。</li></ul><p>本文重点介绍 Docker Compose 应用升级。Mender 的 A&#x2F;B 根文件系统适合操作系统整体升级，但需要磁盘分区、bootloader 和系统镜像适配，不能通过安装一个客户端直接获得，文末会单独说明。</p><h2 id="本文版本和环境"><a href="#本文版本和环境" class="headerlink" title="本文版本和环境"></a>本文版本和环境</h2><p>Mender 的软件仓库、Chart 和命令会继续变化。本文固定以下示例环境，实际部署前应再次检查官方 Supported releases 页面。</p><table><thead><tr><th>项目</th><th>示例值</th><th>部署位置</th></tr></thead><tbody><tr><td>Mender Server</td><td><code>v4.1.0</code> 评估环境</td><td>总部测试服务器</td></tr><tr><td>Mender Helm Chart</td><td>安装时选择稳定版本并固定版本号</td><td>总部 Kubernetes</td></tr><tr><td>Mender Client</td><td>6.x，使用 <code>mender-client4</code> 元包安装</td><td>每台地区服务器</td></tr><tr><td>Docker Compose Update Module</td><td><code>1.0.0</code></td><td>每台地区服务器</td></tr><tr><td>地区服务器系统</td><td>Ubuntu Server 24.04 amd64</td><td>各地区</td></tr><tr><td>容器运行环境</td><td>Docker Engine + Compose v2</td><td>各地区</td></tr><tr><td>构建环境</td><td>Ubuntu 24.04 amd64</td><td>总部 CI&#x2F;CD Runner</td></tr><tr><td>对外域名</td><td><code>mender.example.com</code></td><td>指向总部入口</td></tr><tr><td>设备类型</td><td><code>region-server-amd64</code></td><td>Artifact 与客户端保持一致</td></tr></tbody></table><p>文中的域名、账号、Bucket、密钥、镜像地址和地区编号都是示例，不能原样用于生产。</p><h2 id="Mender各组件分别做什么"><a href="#Mender各组件分别做什么" class="headerlink" title="Mender各组件分别做什么"></a>Mender各组件分别做什么</h2><p>先明确 Mender 中几个容易混淆的概念。</p><table><thead><tr><th>组件或概念</th><th>作用</th></tr></thead><tbody><tr><td>Mender Server</td><td>保存设备、Release、Deployment 和执行状态，向设备提供更新指令</td></tr><tr><td>Mender Client</td><td>周期性连接 Server，下载、验签、安装制品并上报结果</td></tr><tr><td>Mender Artifact</td><td>后缀为 <code>.mender</code> 的发布制品，包含负载、兼容信息、版本和可选脚本</td></tr><tr><td>Release</td><td>Mender Server 中可供发布的软件版本，可以包含适配不同设备类型的 Artifact</td></tr><tr><td>Deployment</td><td>一次具体发布任务，定义把哪个 Release 发给哪些设备</td></tr><tr><td>Device Type</td><td>设备兼容类型，Artifact 只会安装到匹配的设备上</td></tr><tr><td>Inventory</td><td>设备上报的地区、环境、架构和当前版本等属性</td></tr><tr><td>Device Group</td><td>设备集合，作为 Deployment 的发布目标</td></tr><tr><td>Update Module</td><td>Mender Client 的扩展，负责解释和安装某一种 Artifact 负载</td></tr><tr><td>State Script</td><td>在下载、安装、提交、回滚等生命周期执行的自定义脚本</td></tr><tr><td>Mender Gateway</td><td>企业版地区代理和 Artifact 缓存，不是独立发布中心</td></tr><tr><td><code>mender-artifact</code></td><td>创建、读取、签名和验证 <code>.mender</code> 文件的命令行工具</td></tr><tr><td><code>mender-cli</code></td><td>上传 Artifact、调用服务端能力的命令行工具</td></tr></tbody></table><p>在本文场景中，每台地区服务器都作为一台 Mender Device。总部创建 Deployment 后，地区服务器在下一次轮询时收到任务。</p><h2 id="推荐架构"><a href="#推荐架构" class="headerlink" title="推荐架构"></a>推荐架构</h2><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    flowchart TB    subgraph HQ[总部]        GIT[业务代码仓库] --&gt; CI[CI&#x2F;CD Runner]        CI --&gt; BUILD[构建容器镜像和 Compose]        BUILD --&gt; ART[生成并签名 .mender]        ART --&gt; MS[Mender Server]        MS --&gt; DB[(MongoDB)]        MS --&gt; OBJ[(S3 对象存储)]        MS --&gt; CACHE[(Redis)]        MS --&gt; MQ[(NATS)]    end    MS --&gt;|HTTPS 443| E1[华东地区服务器\nMender Client + Compose Module]    MS --&gt;|HTTPS 443| E2[华南地区服务器\nMender Client + Compose Module]    MS --&gt;|HTTPS 443| E3[西北地区服务器\nMender Client + Compose Module]    E1 --&gt; D1[Docker Compose 业务服务]    E2 --&gt; D2[Docker Compose 业务服务]    E3 --&gt; D3[Docker Compose 业务服务]  </pre></div><p>Mender Client 主动发起所有通信，因此地区服务器只需要出站访问总部 <code>443/TCP</code>。总部不需要 SSH 到地区服务器，地区防火墙也不需要为 Mender Client 开放入站端口。</p><p>这里的“同步升级”是同一 Deployment 内的批量升级，不代表所有服务器在同一秒开始。设备何时拿到任务取决于自己的轮询周期。生产环境也不建议同时升级全部地区，而应该先试点再逐步扩大。</p><h2 id="部署位置规划"><a href="#部署位置规划" class="headerlink" title="部署位置规划"></a>部署位置规划</h2><table><thead><tr><th>位置</th><th>必须部署</th><th>可选部署</th></tr></thead><tbody><tr><td>总部 Kubernetes</td><td>Mender Server、Ingress</td><td>Prometheus、日志平台</td></tr><tr><td>总部基础设施</td><td>MongoDB、Redis、NATS、对象存储</td><td>KMS、Vault、企业 SSO</td></tr><tr><td>总部 CI&#x2F;CD Runner</td><td>Docker、Skopeo、<code>mender-artifact</code>、<code>mender-cli</code>、Artifact 生成器</td><td>HSM 或云 KMS 客户端</td></tr><tr><td>每台地区服务器</td><td>Docker、Compose、Mender Client、Compose Update Module、验签公钥</td><td>数据库备份工具、业务预检脚本</td></tr><tr><td>每个地区出口节点</td><td>无</td><td>Mender Gateway，适合设备多或出口受限的地区</td></tr></tbody></table><p>如果使用 Hosted Mender，总部 Kubernetes、MongoDB、Redis、NATS 和对象存储都由 Mender 官方维护。企业只需要部署 CI&#x2F;CD 工具、地区客户端和业务升级脚本。</p><h2 id="从哪里下载"><a href="#从哪里下载" class="headerlink" title="从哪里下载"></a>从哪里下载</h2><p>所有组件都应从官方地址获取。</p><table><thead><tr><th>内容</th><th>官方地址</th></tr></thead><tbody><tr><td>Hosted Mender 注册</td><td><a href="https://mender.io/signup">mender.io&#x2F;signup</a></td></tr><tr><td>Mender 文档</td><td><a href="https://docs.mender.io/">docs.mender.io</a></td></tr><tr><td>Mender Server 源码和评估 Compose</td><td><a href="https://github.com/mendersoftware/mender-server">mendersoftware&#x2F;mender-server</a></td></tr><tr><td>Mender Helm Chart</td><td><a href="https://github.com/mendersoftware/mender-helm">mendersoftware&#x2F;mender-helm</a></td></tr><tr><td>设备端 APT 仓库</td><td><a href="https://docs.mender.io/downloads/device-components">Device components</a></td></tr><tr><td>工作站 APT 仓库</td><td><a href="https://docs.mender.io/downloads/workstation-tools">Workstation tools</a></td></tr><tr><td>Compose Update Module</td><td><a href="https://github.com/mendersoftware/mender-container-modules">mender-container-modules</a></td></tr><tr><td>CI&#x2F;CD 工具镜像</td><td><a href="https://hub.docker.com/r/mendersoftware/mender-ci-tools">mendersoftware&#x2F;mender-ci-tools</a></td></tr><tr><td>Helm Chart 仓库</td><td><code>https://charts.mender.io</code></td></tr></tbody></table><p>生产环境不要使用 <code>experimental</code> APT 源、<code>main</code> 镜像标签或不固定版本的安装脚本输出。应先在测试环境验证，再固定具体软件和 Chart 版本。</p><h2 id="第一阶段：搭建Mender-Server评估环境"><a href="#第一阶段：搭建Mender-Server评估环境" class="headerlink" title="第一阶段：搭建Mender Server评估环境"></a>第一阶段：搭建Mender Server评估环境</h2><p>第一次接触 Mender 时，建议先用 Docker Compose 搭建一次评估环境，熟悉设备接入、上传 Artifact 和创建 Deployment。这个环境配置简单，但证书、依赖和资源配置都不符合生产要求。</p><h3 id="准备总部测试服务器"><a href="#准备总部测试服务器" class="headerlink" title="准备总部测试服务器"></a>准备总部测试服务器</h3><p>官方评估环境最低要求为：</p><ul><li><code>x86_64</code> 或 <code>arm64</code>；</li><li>16 GiB 内存；</li><li>2 个 vCPU；</li><li>50 GiB 可用磁盘；</li><li>Docker Engine；</li><li>Docker Compose 2.23.1 或更新版本。</li></ul><p>确认环境：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker version</span><br><span class="line">docker compose version</span><br><span class="line">free -h</span><br><span class="line"><span class="built_in">df</span> -h</span><br></pre></td></tr></table></figure><p>这一步的作用是确认容器运行环境和磁盘满足 Mender 多个微服务同时启动的要求。</p><h3 id="配置评估域名"><a href="#配置评估域名" class="headerlink" title="配置评估域名"></a>配置评估域名</h3><p>在测试服务器的 <code>/etc/hosts</code> 中增加：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1 docker.mender.io</span><br><span class="line">127.0.0.1 s3.docker.mender.io</span><br></pre></td></tr></table></figure><p>官方评估环境内置证书使用这两个域名。该配置只是让本机能够把域名解析到评估环境，不能作为生产 DNS 方案。</p><h3 id="下载并启动评估服务器"><a href="#下载并启动评估服务器" class="headerlink" title="下载并启动评估服务器"></a>下载并启动评估服务器</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> -b v4.1.0 \</span><br><span class="line">  https://github.com/mendersoftware/mender-server.git \</span><br><span class="line">  mender-server</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> mender-server</span><br><span class="line"><span class="built_in">export</span> MENDER_IMAGE_TAG=v4.1.0</span><br><span class="line">docker compose up -d</span><br></pre></td></tr></table></figure><p>每条命令的作用如下：</p><ul><li><code>git clone -b v4.1.0</code>：下载固定版本的官方 Compose 和配置；</li><li><code>MENDER_IMAGE_TAG</code>：指定各个 Mender 服务使用的镜像版本；</li><li><code>docker compose up -d</code>：后台启动 API Gateway、UI、设备认证、部署、库存和存储等服务。</li></ul><p>检查容器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker compose ps</span><br><span class="line">docker compose logs --<span class="built_in">tail</span>=100</span><br></pre></td></tr></table></figure><p>只有所有关键容器都正常运行后，才继续创建管理员。</p><h3 id="创建管理员"><a href="#创建管理员" class="headerlink" title="创建管理员"></a>创建管理员</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> MENDER_USERNAME=<span class="string">&#x27;admin@docker.mender.io&#x27;</span></span><br><span class="line"><span class="built_in">export</span> MENDER_PASSWORD=<span class="string">&#x27;Replace-With-A-Strong-Password&#x27;</span></span><br><span class="line"></span><br><span class="line">docker compose run --name create-user useradm create-user \</span><br><span class="line">  --username <span class="string">&quot;<span class="variable">$MENDER_USERNAME</span>&quot;</span> \</span><br><span class="line">  --password <span class="string">&quot;<span class="variable">$MENDER_PASSWORD</span>&quot;</span></span><br></pre></td></tr></table></figure><p><code>useradm</code> 是 Mender 的用户管理服务。这条命令直接在服务端创建第一个管理用户。</p><p>浏览器访问：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://localhost</span><br></pre></td></tr></table></figure><p>评估环境使用自签名证书，浏览器会提示不受信任。这个警告只能在隔离测试环境临时接受，生产环境必须配置有效证书。</p><h3 id="停止和清理评估环境"><a href="#停止和清理评估环境" class="headerlink" title="停止和清理评估环境"></a>停止和清理评估环境</h3><p>只停止容器并保留数据：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker compose stop</span><br></pre></td></tr></table></figure><p>删除容器、Volume 和全部测试数据：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker compose down -v --remove-orphans</span><br></pre></td></tr></table></figure><p>第二条命令会删除设备、用户、Artifact、Deployment 和日志，不能在需要保留数据的环境执行。</p><h2 id="第二阶段：生产部署Mender-Server"><a href="#第二阶段：生产部署Mender-Server" class="headerlink" title="第二阶段：生产部署Mender Server"></a>第二阶段：生产部署Mender Server</h2><p>生产环境官方推荐使用 Kubernetes + Helm。经过官方验证和支持的 Kubernetes 环境主要是 EKS、AKS 和 GKE；本地 k3s 可以用于 PoC，但不能因为 Helm 安装成功就视为完整生产集群。</p><h3 id="生产依赖"><a href="#生产依赖" class="headerlink" title="生产依赖"></a>生产依赖</h3><p>Mender Server 生产环境至少需要：</p><table><thead><tr><th>依赖</th><th>作用</th><th>生产建议</th></tr></thead><tbody><tr><td>Kubernetes</td><td>运行 Mender 微服务</td><td>多节点、独立维护、定期升级</td></tr><tr><td>MongoDB</td><td>保存用户、设备、认证、库存和发布元数据</td><td>使用副本集并建立备份</td></tr><tr><td>Redis</td><td>临时数据和缓存</td><td>使用高可用实例</td></tr><tr><td>NATS</td><td>Mender 服务间消息通信</td><td>高可用场景启用 JetStream 副本</td></tr><tr><td>S3 兼容对象存储</td><td>保存 <code>.mender</code> Artifact</td><td>开启版本化、备份和生命周期策略</td></tr><tr><td>Ingress 或 Load Balancer</td><td>对外提供 HTTPS API 和 UI</td><td>只开放 HTTPS，HTTP 跳转 HTTPS</td></tr><tr><td>DNS 和 TLS</td><td>让客户端验证总部服务器身份</td><td>使用企业 CA 或公开可信 CA</td></tr></tbody></table><p>官方 Chart 内可以带 MongoDB、Redis 和 NATS 子 Chart，但这些更适合测试或 PoC。生产环境应使用独立维护的服务。</p><p>Mender Server 本身的参考资源为：</p><table><thead><tr><th>设备规模</th><th>Mender 服务资源，不含数据库和对象存储</th></tr></thead><tbody><tr><td>100 台以内</td><td>8 GiB RAM、4 vCPU</td></tr><tr><td>1 万台以内</td><td>16 GiB RAM、8 vCPU</td></tr><tr><td>10 万台以内</td><td>24 GiB RAM、16 vCPU</td></tr></tbody></table><h3 id="准备Kubernetes和Helm"><a href="#准备Kubernetes和Helm" class="headerlink" title="准备Kubernetes和Helm"></a>准备Kubernetes和Helm</h3><p>如果只是验证 Helm 流程，可以在 Ubuntu 测试机安装 k3s：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">curl -sfL https://get.k3s.io -o install-k3s.sh</span><br><span class="line">less install-k3s.sh</span><br><span class="line"><span class="built_in">sudo</span> sh install-k3s.sh --write-kubeconfig-mode 644</span><br><span class="line"><span class="built_in">export</span> KUBECONFIG=/etc/rancher/k3s/k3s.yaml</span><br><span class="line">kubectl get nodes</span><br></pre></td></tr></table></figure><p>先下载并审查脚本，再以 root 执行。<code>kubectl get nodes</code> 出现 <code>Ready</code> 表示控制面可用。</p><p>安装 Helm：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL \</span><br><span class="line">  https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 \</span><br><span class="line">  -o get-helm-3.sh</span><br><span class="line">less get-helm-3.sh</span><br><span class="line">bash get-helm-3.sh</span><br><span class="line">helm version</span><br></pre></td></tr></table></figure><p>生产环境应使用企业已经维护的 Kubernetes 集群，不应照搬单节点 k3s。</p><h3 id="准备域名和TLS"><a href="#准备域名和TLS" class="headerlink" title="准备域名和TLS"></a>准备域名和TLS</h3><p>将以下域名解析到 Kubernetes Ingress 或 Load Balancer：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mender.example.com</span><br></pre></td></tr></table></figure><p>Ingress 必须满足：</p><ul><li>HTTP 自动跳转 HTTPS；</li><li>对 <code>mender.example.com</code> 提供有效 TLS 证书；</li><li>大文件上传超时足够长；</li><li>如果负载均衡器要求健康检查，使用 <code>/ui</code>；</li><li>Artifact 上传大小不能被 Ingress 默认限制拦截。</li></ul><p>可以使用 cert-manager 自动申请证书：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">helm repo add jetstack https://charts.jetstack.io</span><br><span class="line">helm repo update</span><br><span class="line">helm upgrade --install cert-manager jetstack/cert-manager \</span><br><span class="line">  --namespace cert-manager \</span><br><span class="line">  --create-namespace \</span><br><span class="line">  --version v1.16.1 \</span><br><span class="line">  --<span class="built_in">set</span> crds.enabled=<span class="literal">true</span></span><br></pre></td></tr></table></figure><p>这一步的作用是让 Kubernetes 自动申请、保存和续期 X.509 证书。实际使用 Let’s Encrypt 还是企业 CA，应由网络和安全策略决定。</p><h3 id="准备对象存储"><a href="#准备对象存储" class="headerlink" title="准备对象存储"></a>准备对象存储</h3><p>生产环境建议使用 AWS S3、Cloudflare R2、Google Cloud Storage 的 S3 兼容接口或 Azure Blob Storage。对象存储负责保存真正体积较大的 <code>.mender</code> 文件，MongoDB 只保存元数据。</p><p>准备以下信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> STORAGE_BUCKET=<span class="string">&#x27;mender-artifacts-prod&#x27;</span></span><br><span class="line"><span class="built_in">export</span> STORAGE_ENDPOINT=<span class="string">&#x27;https://s3.example.com&#x27;</span></span><br><span class="line"><span class="built_in">export</span> AWS_REGION=<span class="string">&#x27;us-east-1&#x27;</span></span><br><span class="line"><span class="built_in">export</span> AWS_ACCESS_KEY_ID=<span class="string">&#x27;Replace-With-Access-Key&#x27;</span></span><br><span class="line"><span class="built_in">export</span> AWS_SECRET_ACCESS_KEY=<span class="string">&#x27;Replace-With-Secret-Key&#x27;</span></span><br></pre></td></tr></table></figure><p>生产中不要把密钥写进 Shell 历史或提交到 Git，应放在 Kubernetes Secret、Vault 或云 Secret Manager 中。示例使用环境变量只是为了说明 Chart 需要哪些值。</p><h3 id="准备Mender-Helm配置"><a href="#准备Mender-Helm配置" class="headerlink" title="准备Mender Helm配置"></a>准备Mender Helm配置</h3><p>先添加 Mender Chart 仓库并查看可用版本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">helm repo add mender https://charts.mender.io</span><br><span class="line">helm repo update</span><br><span class="line">helm search repo mender/mender --versions</span><br></pre></td></tr></table></figure><p>创建 <code>mender-values.yml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">ingress:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">ingressClassName:</span> <span class="string">nginx</span></span><br><span class="line">  <span class="attr">annotations:</span></span><br><span class="line">    <span class="attr">cert-manager.io/cluster-issuer:</span> <span class="string">letsencrypt-prod</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/proxy-body-size:</span> <span class="string">&quot;0&quot;</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/proxy-read-timeout:</span> <span class="string">&quot;7200&quot;</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/proxy-send-timeout:</span> <span class="string">&quot;7200&quot;</span></span><br><span class="line">  <span class="attr">path:</span> <span class="string">/</span></span><br><span class="line">  <span class="attr">hosts:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">mender.example.com</span></span><br><span class="line">  <span class="attr">tls:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">secretName:</span> <span class="string">mender-ingress-tls</span></span><br><span class="line">      <span class="attr">hosts:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">mender.example.com</span></span><br><span class="line"></span><br><span class="line"><span class="attr">global:</span></span><br><span class="line">  <span class="attr">url:</span> <span class="string">https://mender.example.com</span></span><br><span class="line">  <span class="attr">s3:</span></span><br><span class="line">    <span class="attr">AWS_URI:</span> <span class="string">https://s3.example.com</span></span><br><span class="line">    <span class="attr">AWS_BUCKET:</span> <span class="string">mender-artifacts-prod</span></span><br><span class="line">    <span class="attr">AWS_REGION:</span> <span class="string">us-east-1</span></span><br><span class="line">    <span class="attr">AWS_ACCESS_KEY_ID:</span> <span class="string">Replace-With-Access-Key</span></span><br><span class="line">    <span class="attr">AWS_SECRET_ACCESS_KEY:</span> <span class="string">Replace-With-Secret-Key</span></span><br><span class="line"></span><br><span class="line"><span class="attr">api_gateway:</span></span><br><span class="line">  <span class="attr">storage_proxy:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">https://s3.example.com</span></span><br><span class="line">    <span class="attr">customRule:</span> <span class="string">&quot;PathRegexp(`^/mender-artifacts-prod`)&quot;</span></span><br></pre></td></tr></table></figure><p>这份配置完成三件事：</p><ol><li>通过 Ingress 暴露 Mender UI 和 API；</li><li>告诉 Mender 自己的外部访问地址；</li><li>指定 Artifact 保存到哪个对象存储。</li></ol><p>不同云厂商、Ingress Controller 和对象存储的配置不完全相同。上面的文件是结构示例，生产部署应按实际服务修改，并将凭据改为 Secret 引用。</p><h3 id="使用外部MongoDB"><a href="#使用外部MongoDB" class="headerlink" title="使用外部MongoDB"></a>使用外部MongoDB</h3><p>创建保存连接串的 Secret：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">kubectl create namespace mender</span><br><span class="line"></span><br><span class="line">kubectl -n mender create secret generic mender-mongo \</span><br><span class="line">  --from-literal=MONGO=<span class="string">&#x27;mongodb://mender-user:Replace-With-Password@mongo-1.example.com:27017,mongo-2.example.com:27017,mongo-3.example.com:27017/mender?replicaSet=rs0&#x27;</span> \</span><br><span class="line">  --from-literal=MONGO_URL=<span class="string">&#x27;mongodb://mender-user:Replace-With-Password@mongo-1.example.com:27017,mongo-2.example.com:27017,mongo-3.example.com:27017/mender?replicaSet=rs0&#x27;</span></span><br></pre></td></tr></table></figure><p>在 <code>mender-values.yml</code> 中增加以下内容；如果文件已经存在 <code>global</code>，必须把 <code>mongodb</code> 合并到原有 <code>global</code> 下，不能再创建一个同级重复键：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">global:</span></span><br><span class="line">  <span class="attr">mongodb:</span></span><br><span class="line">    <span class="attr">existingSecret:</span> <span class="string">mender-mongo</span></span><br><span class="line"></span><br><span class="line"><span class="attr">mongodb:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><p><code>existingSecret</code> 让 Mender 服务读取外部 MongoDB 连接；<code>mongodb.enabled: false</code> 禁用 Chart 内置的评估数据库。</p><p>Redis、NATS 和对象存储也应采用同样原则：连接企业维护的外部服务，并关闭对应的测试子 Chart。具体字段随 Chart 版本变化，应以安装版本的 <code>values.yaml</code> 为准：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">helm show values mender/mender &gt; mender-default-values.yml</span><br></pre></td></tr></table></figure><h3 id="安装Mender-Server"><a href="#安装Mender-Server" class="headerlink" title="安装Mender Server"></a>安装Mender Server</h3><p>前面已经添加过 Mender Chart 仓库。先确认列表，从中选择经过测试的版本，然后固定 Chart：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> MENDER_CHART_VERSION=<span class="string">&#x27;Replace-With-Verified-Chart-Version&#x27;</span></span><br><span class="line"></span><br><span class="line">helm upgrade --install mender mender/mender \</span><br><span class="line">  --namespace mender \</span><br><span class="line">  --create-namespace \</span><br><span class="line">  --version <span class="string">&quot;<span class="variable">$MENDER_CHART_VERSION</span>&quot;</span> \</span><br><span class="line">  --<span class="built_in">wait</span> \</span><br><span class="line">  --<span class="built_in">timeout</span> 20m \</span><br><span class="line">  -f mender-values.yml</span><br></pre></td></tr></table></figure><p>不要在生产中省略 <code>--version</code>。固定 Chart 可以避免某次重新部署时自动拿到不兼容的新版本。</p><p>检查部署：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">kubectl -n mender get pods</span><br><span class="line">kubectl -n mender get ingress</span><br><span class="line">kubectl -n mender get events --sort-by=.lastTimestamp</span><br></pre></td></tr></table></figure><p>所有核心 Pod 应为 <code>Running</code> 或完成预期 Job，Ingress 应显示正确域名和地址。</p><h3 id="创建第一个管理员"><a href="#创建第一个管理员" class="headerlink" title="创建第一个管理员"></a>创建第一个管理员</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">USERADM_POD=$(kubectl -n mender get pod \</span><br><span class="line">  -l <span class="string">&#x27;app.kubernetes.io/component=useradm&#x27;</span> \</span><br><span class="line">  -o name | <span class="built_in">head</span> -1)</span><br><span class="line"></span><br><span class="line">kubectl -n mender <span class="built_in">exec</span> <span class="string">&quot;<span class="variable">$USERADM_POD</span>&quot;</span> -- \</span><br><span class="line">  useradm create-user \</span><br><span class="line">  --username <span class="string">&#x27;admin@example.com&#x27;</span> \</span><br><span class="line">  --password <span class="string">&#x27;Replace-With-A-Strong-Password&#x27;</span></span><br></pre></td></tr></table></figure><p>浏览器访问：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://mender.example.com</span><br></pre></td></tr></table></figure><p>如果页面打不开，依次检查 DNS、Ingress 地址、TLS Secret、Ingress 日志和 Mender Pod 状态。</p><h3 id="生产备份"><a href="#生产备份" class="headerlink" title="生产备份"></a>生产备份</h3><p>Mender Server 至少需要分别备份：</p><ul><li>MongoDB：设备身份、用户、库存、Deployment 和发布元数据；</li><li>对象存储：全部 <code>.mender</code> Artifact；</li><li>Kubernetes Secret：服务连接串、证书和部署配置；</li><li>Helm values：去除明文密钥后纳入配置版本管理。</li></ul><p>只备份 MongoDB 不能恢复 Artifact 文件，只备份对象存储也不能恢复设备和发布记录。恢复演练必须验证 UI 登录、设备认证、Artifact 下载和历史 Deployment 查询。</p><h2 id="第三阶段：安装地区服务器Mender-Client"><a href="#第三阶段：安装地区服务器Mender-Client" class="headerlink" title="第三阶段：安装地区服务器Mender Client"></a>第三阶段：安装地区服务器Mender Client</h2><p>下面的操作在每一台地区服务器执行。</p><h3 id="检查网络和时间"><a href="#检查网络和时间" class="headerlink" title="检查网络和时间"></a>检查网络和时间</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">getent hosts mender.example.com</span><br><span class="line">curl -I https://mender.example.com/ui</span><br><span class="line">timedatectl status</span><br></pre></td></tr></table></figure><p>这一步验证三个前提：</p><ul><li>DNS 能解析总部域名；</li><li>出站 HTTPS 可用；</li><li>系统时间正确，TLS 和 Artifact 有效期判断不会出错。</li></ul><p>地区服务器无需开放 Mender 入站端口。</p><h3 id="安装Docker和Compose"><a href="#安装Docker和Compose" class="headerlink" title="安装Docker和Compose"></a>安装Docker和Compose</h3><p>按照 Docker 官方 Ubuntu 安装文档安装 Docker Engine 和 Compose 插件，然后验证：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker version</span><br><span class="line">docker compose version</span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> --now docker</span><br></pre></td></tr></table></figure><p>Compose Update Module 会调用本机 Docker，因此 Docker 必须由 systemd 正常管理。</p><h3 id="安装Mender-Client"><a href="#安装Mender-Client" class="headerlink" title="安装Mender Client"></a>安装Mender Client</h3><p>下面两种方式二选一。最快的方式是官方便利脚本，适合 PoC；先下载、审查，再执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">curl -fLsS https://get.mender.io -o get-mender.sh</span><br><span class="line">less get-mender.sh</span><br><span class="line"><span class="built_in">sudo</span> bash get-mender.sh mender-client4</span><br></pre></td></tr></table></figure><p>只传入 <code>mender-client4</code>，可以避免便利脚本额外安装 Remote Terminal 和 Configure 插件。</p><p>生产环境更推荐显式配置官方 <code>device-components</code> APT 仓库并固定软件版本。Ubuntu 24.04 可以执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get update</span><br><span class="line"><span class="built_in">sudo</span> apt-get install --assume-yes --no-install-recommends \</span><br><span class="line">  apt-transport-https ca-certificates curl gnupg jq</span><br><span class="line"></span><br><span class="line">curl -fsSL https://downloads.mender.io/repos/debian/gpg \</span><br><span class="line">  | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/trusted.gpg.d/mender.asc &gt;/dev/null</span><br><span class="line"></span><br><span class="line">gpg --show-keys --with-fingerprint \</span><br><span class="line">  /etc/apt/trusted.gpg.d/mender.asc</span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;deb [arch=<span class="subst">$(dpkg --print-architecture)</span>] https://downloads.mender.io/repos/device-components ubuntu/noble/stable main&quot;</span> \</span><br><span class="line">  | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/sources.list.d/mender-device-components.list</span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> apt-get update</span><br><span class="line">apt-cache madison mender-client4</span><br></pre></td></tr></table></figure><p>从 <code>apt-cache</code> 输出中选定经过测试的版本。首次验证时可以安装仓库当前稳定版：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get install --assume-yes mender-client4</span><br></pre></td></tr></table></figure><p>完成兼容性验证后，应在自动化安装清单中记录实际包版本，并通过 APT preference 或版本参数锁定，避免地区服务器在不同日期安装到不同客户端版本。</p><p>官方仓库签名密钥指纹应核对为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">E6C8 5734 5575 F921 8396 5662 2407 2B80 A1B2 9B00</span><br></pre></td></tr></table></figure><p>安装后检查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">mender-update --version</span><br><span class="line">mender-auth --version</span><br><span class="line">mender-setup --<span class="built_in">help</span></span><br><span class="line">systemctl status mender-authd mender-updated</span><br></pre></td></tr></table></figure><p><code>mender-authd</code> 负责设备认证和令牌，<code>mender-updated</code> 负责轮询、下载和安装更新。</p><h3 id="配置连接总部"><a href="#配置连接总部" class="headerlink" title="配置连接总部"></a>配置连接总部</h3><p>定义设备类型和服务器地址：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> DEVICE_TYPE=<span class="string">&#x27;region-server-amd64&#x27;</span></span><br><span class="line"><span class="built_in">export</span> MENDER_SERVER_URL=<span class="string">&#x27;https://mender.example.com&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> mender-setup \</span><br><span class="line">  --device-type <span class="string">&quot;<span class="variable">$DEVICE_TYPE</span>&quot;</span> \</span><br><span class="line">  --server-url <span class="string">&quot;<span class="variable">$MENDER_SERVER_URL</span>&quot;</span></span><br></pre></td></tr></table></figure><p><code>device-type</code> 必须和后面生成 Artifact 时的兼容类型完全一致。不同 CPU 架构或部署形态应使用不同类型，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">region-server-amd64</span><br><span class="line">region-server-arm64</span><br><span class="line">factory-edge-amd64</span><br></pre></td></tr></table></figure><p>生产环境不要使用 <code>--demo-polling</code>，它会产生比默认值更频繁的请求。</p><p>如果使用 Hosted Mender，不传 <code>--server-url</code>，而是从 Organization 设置中取得 Tenant Token：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> DEVICE_TYPE=<span class="string">&#x27;region-server-amd64&#x27;</span></span><br><span class="line"><span class="built_in">export</span> TENANT_TOKEN=<span class="string">&#x27;Replace-With-Tenant-Token&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> mender-setup \</span><br><span class="line">  --device-type <span class="string">&quot;<span class="variable">$DEVICE_TYPE</span>&quot;</span> \</span><br><span class="line">  --hosted-mender \</span><br><span class="line">  --tenant-token <span class="string">&quot;<span class="variable">$TENANT_TOKEN</span>&quot;</span></span><br></pre></td></tr></table></figure><p>Tenant Token 能让新设备向组织申请接入，必须作为 Secret 管理，不能写入镜像、Git 或日志。Hosted 和自建 Server 两种配置方式同样是二选一。</p><h3 id="配置轮询和证书"><a href="#配置轮询和证书" class="headerlink" title="配置轮询和证书"></a>配置轮询和证书</h3><p>Mender Client 主配置位于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/etc/mender/mender.conf</span><br></pre></td></tr></table></figure><p>默认更新轮询周期是 1800 秒，Inventory 上报周期是 28800 秒。可以调整为：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;ServerURL&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://mender.example.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;UpdatePollIntervalSeconds&quot;</span><span class="punctuation">:</span> <span class="number">300</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;InventoryPollIntervalSeconds&quot;</span><span class="punctuation">:</span> <span class="number">3600</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这只是最小结构示例。编辑时要保留 <code>mender-setup</code> 已写入的其他字段，不能直接覆盖整份配置。</p><ul><li><code>UpdatePollIntervalSeconds: 300</code>：设备最多约 5 分钟发现新任务；</li><li><code>InventoryPollIntervalSeconds: 3600</code>：每小时上报一次地区和版本信息；</li><li>轮询越频繁，总部服务端压力越大。</li></ul><p>如果总部使用公开可信 CA，客户端使用系统 CA 即可。如果使用企业私有 CA，应把 CA 证书放到客户端，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/etc/mender/server.crt</span><br></pre></td></tr></table></figure><p>然后在配置中增加：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;ServerCertificate&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/etc/mender/server.crt&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>不要在生产环境启用跳过 TLS 验证。</p><p>重新启动客户端：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart mender-authd mender-updated</span><br><span class="line"><span class="built_in">sudo</span> journalctl -u mender-authd -n 100 --no-pager</span><br><span class="line"><span class="built_in">sudo</span> journalctl -u mender-updated -n 100 --no-pager</span><br></pre></td></tr></table></figure><h3 id="在服务端接纳设备"><a href="#在服务端接纳设备" class="headerlink" title="在服务端接纳设备"></a>在服务端接纳设备</h3><p>客户端第一次运行时会生成自己的密钥：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/var/lib/mender/mender-agent.pem</span><br></pre></td></tr></table></figure><p>然后向 Server 提交认证请求。在 Mender UI 中进入 Devices，找到 Pending 设备，核对设备 ID、公钥和主机信息后选择 Accept。</p><p>只有 Accepted 设备才能进入 Deployment。生产环境设备多时，可以使用预授权 API 或企业版 mTLS 自动接纳，但不能在没有身份校验的情况下自动接受任意设备。</p><h2 id="第四阶段：上报地区Inventory并建立分组"><a href="#第四阶段：上报地区Inventory并建立分组" class="headerlink" title="第四阶段：上报地区Inventory并建立分组"></a>第四阶段：上报地区Inventory并建立分组</h2><p>Mender 会执行 <code>/usr/share/mender/inventory</code> 中所有以 <code>mender-inventory-</code> 开头的可执行文件，并把脚本标准输出解析为 Inventory。</p><p>在华东服务器创建：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">tee</span> /usr/share/mender/inventory/mender-inventory-region &gt;/dev/null &lt;&lt;<span class="string">&#x27;EOF&#x27;</span></span><br><span class="line"><span class="comment">#!/bin/sh</span></span><br><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> \</span><br><span class="line">  <span class="string">&#x27;region=east-china&#x27;</span> \</span><br><span class="line">  <span class="string">&#x27;environment=production&#x27;</span> \</span><br><span class="line">  <span class="string">&#x27;site_id=site-001&#x27;</span> \</span><br><span class="line">  <span class="string">&#x27;service_type=factory-compose&#x27;</span></span><br><span class="line">EOF</span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 700 /usr/share/mender/inventory/mender-inventory-region</span><br></pre></td></tr></table></figure><p>手工检查脚本输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> /usr/share/mender/inventory/mender-inventory-region</span><br></pre></td></tr></table></figure><p>这些属性用于搜索、筛选和分组，不用于唯一识别设备。<code>site_id</code> 也不应代替 Mender Device ID。</p><p>等待 Inventory 上报后，在 UI 中可以：</p><ol><li>建立静态组 <code>pilot</code>，手工加入试点服务器；</li><li>建立静态组 <code>production-east</code>、<code>production-south</code> 等地区组；</li><li>企业版可根据 <code>region</code> 和 <code>environment</code> 建立动态组。</li></ol><p>静态组在创建 Deployment 时会固化设备列表。Deployment 创建后再向组里增加设备，不会把新设备自动加入正在执行的任务。</p><h2 id="第五阶段：安装Docker-Compose-Update-Module"><a href="#第五阶段：安装Docker-Compose-Update-Module" class="headerlink" title="第五阶段：安装Docker Compose Update Module"></a>第五阶段：安装Docker Compose Update Module</h2><p>只安装 Mender Client 还不能理解 <code>docker-compose</code> Artifact。需要在每台地区服务器安装对应 Update Module。</p><h3 id="下载并安装模块"><a href="#下载并安装模块" class="headerlink" title="下载并安装模块"></a>下载并安装模块</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get update</span><br><span class="line"><span class="built_in">sudo</span> apt-get install --assume-yes git make m4</span><br><span class="line"></span><br><span class="line">git <span class="built_in">clone</span> --branch 1.0.0 --depth 1 \</span><br><span class="line">  https://github.com/mendersoftware/mender-container-modules.git</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> mender-container-modules</span><br><span class="line">make</span><br><span class="line"><span class="built_in">sudo</span> make install-docker-compose</span><br></pre></td></tr></table></figure><p>这些命令的作用分别是：</p><ul><li>下载固定版本的官方容器更新模块；</li><li><code>make</code> 将模板和公共 Shell 逻辑生成可执行模块；</li><li><code>make install-docker-compose</code> 安装到 Mender 的 Update Module 目录。</li></ul><p>验证：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">test</span> -x /usr/share/mender/modules/v3/docker-compose</span><br><span class="line"><span class="built_in">ls</span> -l /usr/share/mender/modules/v3/docker-compose</span><br></pre></td></tr></table></figure><p>Update Module 的文件名就是 Artifact payload type。客户端收到 <code>docker-compose</code> 类型负载时，会调用这个文件完成安装、提交或回滚。</p><h3 id="规划Docker持久化目录"><a href="#规划Docker持久化目录" class="headerlink" title="规划Docker持久化目录"></a>规划Docker持久化目录</h3><p>如果未来还要做 A&#x2F;B 操作系统升级，Docker 镜像和业务数据不能留在会被整体替换的根分区。应把 Docker <code>data-root</code> 放到持久化数据分区，例如：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;data-root&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/data/docker&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>文件位置：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/etc/docker/daemon.json</span><br></pre></td></tr></table></figure><p>修改后需要重启 Docker：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart docker</span><br><span class="line">docker info | grep <span class="string">&#x27;Docker Root Dir&#x27;</span></span><br></pre></td></tr></table></figure><p>只做应用更新时也建议将数据库、上传文件、运行时配置和备份放在明确的持久化目录，不能依赖容器可写层。</p><h2 id="第六阶段：在构建机安装Mender工具"><a href="#第六阶段：在构建机安装Mender工具" class="headerlink" title="第六阶段：在构建机安装Mender工具"></a>第六阶段：在构建机安装Mender工具</h2><p>下面的操作在总部 CI&#x2F;CD Runner 或独立签名机构执行，不是在地区服务器执行。</p><h3 id="配置工作站APT仓库"><a href="#配置工作站APT仓库" class="headerlink" title="配置工作站APT仓库"></a>配置工作站APT仓库</h3><p>安装依赖并导入官方签名密钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get update</span><br><span class="line"><span class="built_in">sudo</span> apt-get install --assume-yes \</span><br><span class="line">  apt-transport-https ca-certificates curl gnupg jq skopeo</span><br><span class="line"></span><br><span class="line">curl -fsSL https://downloads.mender.io/repos/debian/gpg \</span><br><span class="line">  | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/trusted.gpg.d/mender.asc &gt;/dev/null</span><br><span class="line"></span><br><span class="line">gpg --show-keys --with-fingerprint \</span><br><span class="line">  /etc/apt/trusted.gpg.d/mender.asc</span><br></pre></td></tr></table></figure><p>确认指纹为官方公布的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">E6C8 5734 5575 F921 8396 5662 2407 2B80 A1B2 9B00</span><br></pre></td></tr></table></figure><p>Ubuntu 24.04 添加工作站工具源：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&quot;deb [arch=<span class="subst">$(dpkg --print-architecture)</span>] https://downloads.mender.io/repos/workstation-tools ubuntu/noble/stable main&quot;</span> \</span><br><span class="line">  | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/sources.list.d/mender-workstation-tools.list</span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> apt-get update</span><br><span class="line"><span class="built_in">sudo</span> apt-get install --assume-yes mender-artifact mender-cli</span><br></pre></td></tr></table></figure><p>验证：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mender-artifact --version</span><br><span class="line">mender-cli --version</span><br><span class="line">skopeo --version</span><br></pre></td></tr></table></figure><p><code>mender-artifact</code> 处理 <code>.mender</code> 文件，<code>mender-cli</code> 与 Mender Server API 通信，<code>skopeo</code> 在不运行容器的情况下下载不同架构的镜像。</p><p>也可以直接使用官方 CI 工具镜像：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull mendersoftware/mender-ci-tools:1.0.0</span><br></pre></td></tr></table></figure><h3 id="下载Compose-Artifact生成器"><a href="#下载Compose-Artifact生成器" class="headerlink" title="下载Compose Artifact生成器"></a>下载Compose Artifact生成器</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> curl -fL \</span><br><span class="line">  https://raw.githubusercontent.com/mendersoftware/mender-container-modules/1.0.0/src/gen_docker-compose \</span><br><span class="line">  -o /usr/local/bin/gen_docker-compose</span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 755 /usr/local/bin/gen_docker-compose</span><br><span class="line">gen_docker-compose --<span class="built_in">help</span></span><br></pre></td></tr></table></figure><p>生成器负责解析 Compose、通过 Skopeo 下载镜像，再把 Compose 和镜像归档成 Mender Artifact。</p><h2 id="第七阶段：准备业务Compose发布包"><a href="#第七阶段：准备业务Compose发布包" class="headerlink" title="第七阶段：准备业务Compose发布包"></a>第七阶段：准备业务Compose发布包</h2><p>创建发布目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p release/manifests release/output</span><br><span class="line"><span class="built_in">cd</span> release</span><br></pre></td></tr></table></figure><p>示例 <code>manifests/compose.yaml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">frontend:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">registry.example.com/factory/frontend:2026.08.1</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080:80&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/data/factory/runtime-config.json:/usr/share/nginx/html/runtime-config.json:ro</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;wget&quot;</span>, <span class="string">&quot;-q&quot;</span>, <span class="string">&quot;--spider&quot;</span>, <span class="string">&quot;http://127.0.0.1/healthz&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br><span class="line">      <span class="attr">retries:</span> <span class="number">12</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">backend:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">registry.example.com/factory/backend:2026.08.1</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/data/factory/backend-config.yaml:/app/config.yaml:ro</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;wget&quot;</span>, <span class="string">&quot;-q&quot;</span>, <span class="string">&quot;--spider&quot;</span>, <span class="string">&quot;http://127.0.0.1:8080/health/ready&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br><span class="line">      <span class="attr">retries:</span> <span class="number">12</span></span><br></pre></td></tr></table></figure><p>发布前需要完成以下应用改造：</p><ul><li>镜像使用不可变版本标签，正式环境最好同时记录 digest；</li><li>地区差异配置通过 Volume 在运行时挂载；</li><li>前端提供 <code>/healthz</code> 和 <code>/version.json</code>；</li><li>后端提供 <code>/health/live</code>、<code>/health/ready</code> 和 <code>/version</code>；</li><li>数据库、用户上传和业务文件位于宿主持久化目录或外部服务；</li><li>Compose 设置明确的 restart 和 healthcheck；</li><li>同一个镜像可以部署到所有地区，不能为每个地区重新构建。</li></ul><p>如果私有 Registry 需要认证，先在构建机登录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">skopeo login registry.example.com</span><br></pre></td></tr></table></figure><p>认证只用于构建机下载镜像。因为最终 Artifact 可以包含镜像归档，地区服务器不一定需要访问 Registry。</p><h2 id="第八阶段：生成Mender-Artifact"><a href="#第八阶段：生成Mender-Artifact" class="headerlink" title="第八阶段：生成Mender Artifact"></a>第八阶段：生成Mender Artifact</h2><p>先检查镜像支持的架构：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">gen_docker-compose \</span><br><span class="line">  --list-architectures \</span><br><span class="line">  --manifests-dir manifests</span><br></pre></td></tr></table></figure><p>生成 amd64 Artifact：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">gen_docker-compose \</span><br><span class="line">  --artifact-name factory-release-2026.08.1 \</span><br><span class="line">  --device-type region-server-amd64 \</span><br><span class="line">  --architecture amd64 \</span><br><span class="line">  --manifests-dir manifests \</span><br><span class="line">  --project-name factory-service \</span><br><span class="line">  --output-path output/factory-release-2026.08.1.mender \</span><br><span class="line">  -- \</span><br><span class="line">  --software-filesystem data-docker</span><br></pre></td></tr></table></figure><p>参数作用如下：</p><table><thead><tr><th>参数</th><th>作用</th></tr></thead><tbody><tr><td><code>--artifact-name</code></td><td>设备安装后上报的软件版本名</td></tr><tr><td><code>--device-type</code></td><td>限制只有相同类型的设备能够安装</td></tr><tr><td><code>--architecture</code></td><td>指定下载哪个 CPU 架构的容器镜像</td></tr><tr><td><code>--manifests-dir</code></td><td>Compose 文件目录</td></tr><tr><td><code>--project-name</code></td><td>Compose 项目名，决定服务集合的管理范围</td></tr><tr><td><code>--output-path</code></td><td>最终 <code>.mender</code> 文件位置</td></tr><tr><td><code>--software-filesystem</code></td><td>声明容器镜像所在持久化文件系统</td></tr></tbody></table><p>生成器会得到两个主要负载：</p><ul><li><code>images.tar.gz</code>：Compose 引用的容器镜像；</li><li><code>manifests.tar</code>：Compose 文件和相关清单。</li></ul><p>读取 Artifact 信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mender-artifact <span class="built_in">read</span> output/factory-release-2026.08.1.mender</span><br></pre></td></tr></table></figure><p>重点检查：</p><ul><li>Artifact name；</li><li>Compatible devices；</li><li>payload type 是否为 <code>docker-compose</code>；</li><li>架构是否正确；</li><li>Compose 项目名和软件文件系统是否正确。</li></ul><h2 id="第九阶段：签名并在客户端启用强制验签"><a href="#第九阶段：签名并在客户端启用强制验签" class="headerlink" title="第九阶段：签名并在客户端启用强制验签"></a>第九阶段：签名并在客户端启用强制验签</h2><p>TLS 保护传输过程，Artifact 签名则保证制品本身来自受信发布方。两者不能互相替代。</p><h3 id="生成签名密钥"><a href="#生成签名密钥" class="headerlink" title="生成签名密钥"></a>生成签名密钥</h3><p>在独立签名机生成 RSA 3072 位密钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">openssl genpkey \</span><br><span class="line">  -algorithm RSA \</span><br><span class="line">  -out private.key \</span><br><span class="line">  -pkeyopt rsa_keygen_bits:3072</span><br><span class="line"></span><br><span class="line">openssl rsa -<span class="keyword">in</span> private.key -out public.key -pubout</span><br><span class="line"><span class="built_in">chmod</span> 600 private.key</span><br></pre></td></tr></table></figure><ul><li><code>private.key</code>：只保存在签名机、KMS、Vault 或 HSM，不能进入 Git 和地区服务器；</li><li><code>public.key</code>：部署到所有地区服务器，用于验证签名。</li></ul><h3 id="签名Artifact"><a href="#签名Artifact" class="headerlink" title="签名Artifact"></a>签名Artifact</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">mender-artifact sign \</span><br><span class="line">  output/factory-release-2026.08.1.mender \</span><br><span class="line">  -k private.key \</span><br><span class="line">  -o output/factory-release-2026.08.1-signed.mender</span><br></pre></td></tr></table></figure><p>验证：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mender-artifact validate \</span><br><span class="line">  output/factory-release-2026.08.1-signed.mender \</span><br><span class="line">  -k public.key</span><br></pre></td></tr></table></figure><p>只有本地验证成功，才能上传到生产 Mender Server。</p><h3 id="客户端安装验签公钥"><a href="#客户端安装验签公钥" class="headerlink" title="客户端安装验签公钥"></a>客户端安装验签公钥</h3><p>在每台地区服务器执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> install -o root -g root -m 0644 \</span><br><span class="line">  public.key \</span><br><span class="line">  /etc/mender/artifact-verify-key.pem</span><br></pre></td></tr></table></figure><p>在 <code>/etc/mender/mender.conf</code> 的现有 JSON 中加入：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;ArtifactVerifyKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/etc/mender/artifact-verify-key.pem&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>重新启动：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart mender-updated</span><br></pre></td></tr></table></figure><p>一旦启用 <code>ArtifactVerifyKey</code>，客户端会拒绝未签名 Artifact 和签名不匹配的 Artifact。公钥轮换期间可以使用 <code>ArtifactVerifyKeys</code> 同时配置新旧公钥，确认全部设备获得新公钥后再删除旧公钥。</p><h2 id="第十阶段：加入数据库备份、迁移和健康检查"><a href="#第十阶段：加入数据库备份、迁移和健康检查" class="headerlink" title="第十阶段：加入数据库备份、迁移和健康检查"></a>第十阶段：加入数据库备份、迁移和健康检查</h2><p>Mender 能触发回滚，但它不知道数据库语义，也不会自动撤销 State Script 对数据库做出的修改。数据库升级必须自己设计。</p><h3 id="State-Script命名"><a href="#State-Script命名" class="headerlink" title="State Script命名"></a>State Script命名</h3><p>Artifact State Script 使用以下格式：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;STATE_NAME&gt;_&lt;ACTION&gt;_&lt;ORDER&gt;_&lt;DESCRIPTION&gt;</span><br></pre></td></tr></table></figure><p>典型脚本：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">ArtifactInstall_Enter_10_backup-database</span><br><span class="line">ArtifactInstall_Leave_20_migrate-database</span><br><span class="line">ArtifactCommit_Enter_10_health-check</span><br><span class="line">ArtifactRollback_Enter_10_restore-database</span><br></pre></td></tr></table></figure><p>脚本退出码：</p><table><thead><tr><th>退出码</th><th>含义</th></tr></thead><tbody><tr><td><code>0</code></td><td>成功，继续升级</td></tr><tr><td><code>1</code></td><td>失败，进入回滚或失败流程</td></tr><tr><td><code>21</code></td><td>稍后重试，受重试间隔和总超时限制</td></tr></tbody></table><p>脚本必须满足：</p><ul><li>幂等，多次运行结果一致；</li><li>有明确超时；</li><li>诊断信息写到标准错误；</li><li>不把密码打印到日志；</li><li>备份放在持久化目录；</li><li>每次备份与 Deployment 或 release ID 关联；</li><li>实际验证备份可恢复，而不是只判断命令退出码。</li></ul><h3 id="健康检查示例"><a href="#健康检查示例" class="headerlink" title="健康检查示例"></a>健康检查示例</h3><p><code>ArtifactCommit_Enter_10_health-check</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line"><span class="built_in">set</span> -eu</span><br><span class="line"></span><br><span class="line">attempt=1</span><br><span class="line"><span class="keyword">while</span> [ <span class="string">&quot;<span class="variable">$attempt</span>&quot;</span> -le 30 ]; <span class="keyword">do</span></span><br><span class="line">  <span class="keyword">if</span> curl --fail --silent --show-error \</span><br><span class="line">    http://127.0.0.1:8080/health/ready &gt;/dev/null; <span class="keyword">then</span></span><br><span class="line">    <span class="built_in">exit</span> 0</span><br><span class="line">  <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line">  attempt=$((attempt + <span class="number">1</span>))</span><br><span class="line">  <span class="built_in">sleep</span> 10</span><br><span class="line"><span class="keyword">done</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;backend readiness check failed after 300 seconds&#x27;</span> &gt;&amp;2</span><br><span class="line"><span class="built_in">exit</span> 1</span><br></pre></td></tr></table></figure><p>将健康检查放在 <code>ArtifactCommit_Enter</code>，可以在 Mender 正式提交版本之前阻止错误版本。返回 <code>1</code> 后，Mender 会执行 Update Module 回滚流程。</p><h3 id="将脚本放进Artifact"><a href="#将脚本放进Artifact" class="headerlink" title="将脚本放进Artifact"></a>将脚本放进Artifact</h3><p>给脚本执行权限：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">chmod</span> 755 scripts/ArtifactInstall_Enter_10_backup-database</span><br><span class="line"><span class="built_in">chmod</span> 755 scripts/ArtifactInstall_Leave_20_migrate-database</span><br><span class="line"><span class="built_in">chmod</span> 755 scripts/ArtifactCommit_Enter_10_health-check</span><br><span class="line"><span class="built_in">chmod</span> 755 scripts/ArtifactRollback_Enter_10_restore-database</span><br></pre></td></tr></table></figure><p>生成 Artifact 时，将 <code>--script</code> 参数透传给 <code>mender-artifact</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">gen_docker-compose \</span><br><span class="line">  --artifact-name factory-release-2026.08.1 \</span><br><span class="line">  --device-type region-server-amd64 \</span><br><span class="line">  --architecture amd64 \</span><br><span class="line">  --manifests-dir manifests \</span><br><span class="line">  --project-name factory-service \</span><br><span class="line">  --output-path output/factory-release-2026.08.1.mender \</span><br><span class="line">  -- \</span><br><span class="line">  --software-filesystem data-docker \</span><br><span class="line">  --script scripts/ArtifactInstall_Enter_10_backup-database \</span><br><span class="line">  --script scripts/ArtifactInstall_Leave_20_migrate-database \</span><br><span class="line">  --script scripts/ArtifactCommit_Enter_10_health-check \</span><br><span class="line">  --script scripts/ArtifactRollback_Enter_10_restore-database</span><br></pre></td></tr></table></figure><p>再次读取 Artifact，确认 State scripts 已被打包：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mender-artifact <span class="built_in">read</span> output/factory-release-2026.08.1.mender</span><br></pre></td></tr></table></figure><p>数据库迁移的准确执行时机必须根据业务确定。某些 Compose 安装流程会先启动新容器，因此更稳妥的方案可能是把迁移做成一次性 Compose service，并让业务服务在迁移成功后启动。不可逆迁移不应进入无人值守升级通道。</p><h2 id="第十一步：上传Release"><a href="#第十一步：上传Release" class="headerlink" title="第十一步：上传Release"></a>第十一步：上传Release</h2><h3 id="使用UI上传"><a href="#使用UI上传" class="headerlink" title="使用UI上传"></a>使用UI上传</h3><p>进入 Mender UI：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Releases -&gt; Upload</span><br></pre></td></tr></table></figure><p>选择签名后的文件：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">factory-release-2026.08.1-signed.mender</span><br></pre></td></tr></table></figure><p>上传后展开 Artifact 信息，再次检查设备类型、版本、payload type 和签名状态。</p><h3 id="使用mender-cli上传"><a href="#使用mender-cli上传" class="headerlink" title="使用mender-cli上传"></a>使用mender-cli上传</h3><p>在 Mender UI 的个人资料页面创建 Personal Access Token。Token 只显示一次，应存入 CI Secret。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> MENDER_SERVER_URL=<span class="string">&#x27;https://mender.example.com&#x27;</span></span><br><span class="line"><span class="built_in">export</span> MENDER_PAT=<span class="string">&#x27;Replace-With-Personal-Access-Token&#x27;</span></span><br><span class="line"></span><br><span class="line">mender-cli artifacts \</span><br><span class="line">  --server <span class="string">&quot;<span class="variable">$MENDER_SERVER_URL</span>&quot;</span> \</span><br><span class="line">  --token-value <span class="string">&quot;<span class="variable">$MENDER_PAT</span>&quot;</span> \</span><br><span class="line">  upload output/factory-release-2026.08.1-signed.mender</span><br></pre></td></tr></table></figure><p>PAT 是长期凭据，权限继承创建它的用户。CI 应使用权限最小化的专用账号，并设置过期和轮换策略。</p><h2 id="第十二步：创建灰度Deployment"><a href="#第十二步：创建灰度Deployment" class="headerlink" title="第十二步：创建灰度Deployment"></a>第十二步：创建灰度Deployment</h2><p>推荐发布顺序：</p><ol><li>开发环境；</li><li>测试地区；</li><li><code>pilot</code> 试点组；</li><li>一个低风险生产地区；</li><li>10% 生产设备；</li><li>全量地区。</li></ol><p>在 Mender UI 创建 Deployment：</p><ol><li>选择 <code>factory-release-2026.08.1</code> Release；</li><li>目标选择 <code>pilot</code> 静态组；</li><li>设置开始时间或立即开始；</li><li>检查设备数量；</li><li>创建 Deployment；</li><li>观察下载、安装、成功和失败状态；</li><li>试点观察期结束后，再创建下一批 Deployment。</li></ol><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    sequenceDiagram    participant CI as 总部CI&#x2F;CD    participant MS as Mender Server    participant MC as 地区Mender Client    participant APP as Docker Compose服务    CI-&gt;&gt;MS: 上传已签名Artifact    CI-&gt;&gt;MS: 创建试点Deployment    MC-&gt;&gt;MS: 按轮询周期检查更新    MS--&gt;&gt;MC: 返回Artifact和Deployment信息    MC-&gt;&gt;MC: 下载并验证签名、设备类型    MC-&gt;&gt;APP: 备份、安装Compose、执行迁移    MC-&gt;&gt;APP: 调用健康检查    alt 健康检查成功        MC-&gt;&gt;MS: 上报success    else 安装或检查失败        MC-&gt;&gt;APP: 回滚Compose并恢复数据库        MC-&gt;&gt;MS: 上报failure和日志    end  </pre></div><p>Mender Server 不会让所有地区在同一秒执行。默认更新轮询为 30 分钟，本文示例改为 5 分钟，所以设备一般会在 5 分钟窗口内陆续开始。</p><h3 id="中止和回滚不是同一操作"><a href="#中止和回滚不是同一操作" class="headerlink" title="中止和回滚不是同一操作"></a>中止和回滚不是同一操作</h3><p>发现故障时，应先在 UI 或 API 中停止仍在进行的 Deployment，防止更多设备领取任务。中止任务不会自动把已经成功升级的设备降回旧版本。</p><p>需要人工回滚已经升级成功的设备时，应保留并重新发布上一版签名 Artifact，例如 <code>factory-release-2026.07.4</code>，再为受影响设备创建一个新的 Deployment。数据库只有在旧应用仍兼容当前 schema，或已执行经过验证的向下迁移、备份恢复后，才允许回退应用。</p><p>自动回滚只发生在当前安装流程尚未成功提交，并且 Update Module 或 State Script 报错的情况下。已经标记成功的 Deployment 不能依靠“中止”恢复旧版本。</p><h2 id="第十三步：观察状态和排查失败"><a href="#第十三步：观察状态和排查失败" class="headerlink" title="第十三步：观察状态和排查失败"></a>第十三步：观察状态和排查失败</h2><h3 id="地区服务器日志"><a href="#地区服务器日志" class="headerlink" title="地区服务器日志"></a>地区服务器日志</h3><p>查看认证日志：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> journalctl -u mender-authd -n 200 --no-pager</span><br></pre></td></tr></table></figure><p>查看更新日志：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> journalctl -u mender-updated -n 200 --no-pager</span><br></pre></td></tr></table></figure><p>实时跟踪：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> journalctl -u mender-updated -f</span><br></pre></td></tr></table></figure><p>失败 Deployment 的详细日志还会保存在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/var/lib/mender/deployments.*.log</span><br></pre></td></tr></table></figure><p>Mender 默认保留最近若干部署日志，并在失败时上传日志到服务端。</p><h3 id="查看设备当前软件信息"><a href="#查看设备当前软件信息" class="headerlink" title="查看设备当前软件信息"></a>查看设备当前软件信息</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> mender-update show-provides</span><br></pre></td></tr></table></figure><p>应能看到当前 Artifact 和 Compose 软件版本。服务端 UI 中也可以从 Device 和 Release 页面交叉核对。</p><h3 id="常见故障"><a href="#常见故障" class="headerlink" title="常见故障"></a>常见故障</h3><h4 id="设备一直Pending"><a href="#设备一直Pending" class="headerlink" title="设备一直Pending"></a>设备一直Pending</h4><p>检查：</p><ul><li>管理员是否在 UI 接受设备；</li><li><code>mender-authd</code> 日志是否有 TLS 或认证错误；</li><li>设备时间是否正确；</li><li>是否错误复制了另一台服务器的 <code>/var/lib/mender</code>。</li></ul><p>每台设备必须拥有独立身份。克隆服务器模板时，应在首次启动前清理模板中的 Mender 私钥和状态，再让每台机器自行生成。</p><h4 id="certificate-signed-by-unknown-authority"><a href="#certificate-signed-by-unknown-authority" class="headerlink" title="certificate signed by unknown authority"></a><code>certificate signed by unknown authority</code></h4><p>表示客户端不信任总部 TLS 证书。使用公开 CA 时更新系统 <code>ca-certificates</code>；使用私有 CA 时，将正确 CA 放入 <code>/etc/mender/server.crt</code> 并配置 <code>ServerCertificate</code>。</p><p>不能通过关闭证书验证掩盖问题。</p><h4 id="Artifact显示noartifact"><a href="#Artifact显示noartifact" class="headerlink" title="Artifact显示noartifact"></a>Artifact显示<code>noartifact</code></h4><p>通常是 Artifact 的 compatible device type 与客户端 Device Type 不一致。检查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> /var/lib/mender/device_type</span><br><span class="line">mender-artifact <span class="built_in">read</span> release.mender</span><br></pre></td></tr></table></figure><p>两边的值必须完全相同。</p><h4 id="签名验证失败"><a href="#签名验证失败" class="headerlink" title="签名验证失败"></a>签名验证失败</h4><p>检查：</p><ul><li>上传的是签名后文件，不是原始文件；</li><li>客户端配置指向正确公钥；</li><li>签名后没有再次修改 Artifact；</li><li>公钥轮换期间新旧密钥配置是否完整。</li></ul><h4 id="Compose安装失败"><a href="#Compose安装失败" class="headerlink" title="Compose安装失败"></a>Compose安装失败</h4><p>检查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker compose version</span><br><span class="line">docker info</span><br><span class="line"><span class="built_in">df</span> -h</span><br><span class="line"><span class="built_in">df</span> -i</span><br><span class="line"><span class="built_in">ls</span> -l /usr/share/mender/modules/v3/docker-compose</span><br></pre></td></tr></table></figure><p>常见原因包括磁盘不足、镜像架构错误、端口冲突、Volume 路径不存在、Compose 语法错误和健康检查命令缺失。</p><h4 id="版本出现-INCONSISTENT"><a href="#版本出现-INCONSISTENT" class="headerlink" title="版本出现_INCONSISTENT"></a>版本出现<code>_INCONSISTENT</code></h4><p>这表示上一次部署失败，并且回滚失败或 Update Module 无法完成回滚。此时不能继续自动发布，应先读取设备 Deployment 日志，确认容器、配置和数据库实际处于哪个版本。</p><h2 id="可选：每个地区部署Mender-Gateway"><a href="#可选：每个地区部署Mender-Gateway" class="headerlink" title="可选：每个地区部署Mender Gateway"></a>可选：每个地区部署Mender Gateway</h2><p>当一个地区有很多设备，或者只有地区出口服务器能访问总部时，可以部署 Mender Gateway。</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    flowchart LR    MS[总部Mender Server] &lt;--&gt;|HTTPS| GW[地区Mender Gateway]    GW --&gt;|局域网HTTPS| D1[服务器1]    GW --&gt;|局域网HTTPS| D2[服务器2]    GW --&gt;|局域网HTTPS| D3[服务器3]    GW --&gt; CACHE[(地区Artifact缓存)]  </pre></div><p>Gateway 可以：</p><ul><li>代理设备到总部的 Mender API 请求；</li><li>把 Artifact 下载地址替换为地区网关地址；</li><li>第一次下载后缓存在本地，后续设备复用；</li><li>为网关后的设备统一增加 <code>region</code> 等 Inventory；</li><li>配合 mTLS 自动接纳持有有效客户端证书的设备。</li></ul><p>Mender Gateway 属于 Enterprise 能力。它不是一套独立地区 Mender Server，也不会在总部上传 Release 后立即主动同步全部 Artifact；缓存通常在设备实际请求时产生。</p><p>示例 <code>/etc/mender/mender-gateway.conf</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;HTTPS&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;Enabled&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;Listen&quot;</span><span class="punctuation">:</span> <span class="string">&quot;:443&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;MinimumTLSVersion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1.2&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;ServerCertificate&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/var/lib/mender/server-cert.pem&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;ServerKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/var/lib/mender/server-pkey.pem&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;Features&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;ArtifactsProxy&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;Enabled&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;GatewayURL&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://mender-gateway-east.example.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;DomainWhitelist&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;s3.example.com&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;ArtifactsCache&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;Enabled&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;Path&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/var/cache/mender-gateway&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;SignatureSecret&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Replace-With-Random-Base64-Secret&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;LinkExpireDuration&quot;</span><span class="punctuation">:</span> <span class="string">&quot;30m&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;DeviceSystem&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;Enabled&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;SystemID&quot;</span><span class="punctuation">:</span> <span class="string">&quot;east-region-gateway&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;DefaultInventory&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;Name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;region&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;Value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;east-china&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">      <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;UpstreamServer&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;URL&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://mender.example.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;CACertificate&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/etc/ssl/certs/ca-certificates.crt&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;InsecureSkipVerify&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>部署时还要完成：</p><ul><li>从企业下载区取得与系统匹配的 <code>mender-gateway</code> Debian 包；</li><li>为 Gateway 配置受信 TLS 证书；</li><li>为缓存准备独立磁盘或 Volume；</li><li>将 <code>DomainWhitelist</code> 限制为真实 Artifact 存储域名；</li><li>把地区设备 <code>ServerURL</code> 指向 Gateway；</li><li>Gateway 2.2.0 或更新版本可启用 Prometheus <code>/metrics</code>，应只监听 <code>127.0.0.1:9102</code> 或受信管理网；</li><li>监控缓存磁盘、请求错误率和到总部的连通性。</li></ul><p>如果每个地区只有一台服务器并且网络稳定，Gateway 的收益很低，可以先不部署。</p><h2 id="如果每个地区必须有独立Mender-Server"><a href="#如果每个地区必须有独立Mender-Server" class="headerlink" title="如果每个地区必须有独立Mender Server"></a>如果每个地区必须有独立Mender Server</h2><p>有些企业要求地区断开总部后仍能独立创建发布。此时可以在每个地区部署完整 Mender Server，但这已经是多套独立控制面。</p><p>需要额外开发或建设：</p><ul><li>总部 Artifact 向各地区对象存储复制；</li><li>Release 元数据同步；</li><li>地区 Deployment 创建和状态汇总；</li><li>API Token、账号和权限管理；</li><li>失败补偿和重复同步幂等；</li><li>总部与地区版本冲突处理；</li><li>断网期间操作审计合并。</li></ul><p>Mender Gateway 不能替代这套联邦控制逻辑。除非有明确的地区自治或长期断网要求，否则优先使用一个中心 Mender Server 加多个 Gateway。</p><h2 id="操作系统A-B升级与Compose升级的区别"><a href="#操作系统A-B升级与Compose升级的区别" class="headerlink" title="操作系统A&#x2F;B升级与Compose升级的区别"></a>操作系统A&#x2F;B升级与Compose升级的区别</h2><p>本文安装 Debian 包的方式只能直接获得应用更新能力，不能自动获得完整操作系统回滚。</p><table><thead><tr><th>更新方式</th><th>更新内容</th><th>是否需要重启</th><th>集成成本</th><th>回滚能力</th></tr></thead><tbody><tr><td>Docker Compose Update Module</td><td>前端、后端、容器和 Compose</td><td>通常不需要整机重启</td><td>较低</td><td>由模块和业务脚本共同保证</td></tr><tr><td>自定义 Update Module</td><td>文件、安装包、业务组件</td><td>取决于实现</td><td>中等</td><td>必须自己实现</td></tr><tr><td>A&#x2F;B rootfs</td><td>整个 Linux 根文件系统</td><td>需要</td><td>高</td><td>启动失败可切回旧分区</td></tr></tbody></table><p>A&#x2F;B rootfs 需要：</p><ul><li>两个根文件系统分区；</li><li>持久化数据分区；</li><li>GRUB 或 U-Boot 集成；</li><li>Mender 化的 Debian&#x2F;Yocto 镜像；</li><li>启动成功确认和 boot counter；</li><li>重新刷写或迁移现有服务器磁盘布局。</li></ul><p>对于已经运行 Docker Compose 的普通服务器，建议第一期只做应用升级；操作系统补丁继续使用现有的系统运维工具。等应用发布链路稳定后，再单独评估 rootfs OTA。</p><h2 id="哪些是安装配置，哪些必须自己开发"><a href="#哪些是安装配置，哪些必须自己开发" class="headerlink" title="哪些是安装配置，哪些必须自己开发"></a>哪些是安装配置，哪些必须自己开发</h2><table><thead><tr><th>工作</th><th>类型</th><th>说明</th></tr></thead><tbody><tr><td>Mender Server、Client、CLI 安装</td><td>现成组件</td><td>按官方方式部署和配置</td></tr><tr><td>设备认证、静态分组、Deployment、状态上报</td><td>现成能力</td><td>Open Source 已覆盖基础流程</td></tr><tr><td>Gateway、mTLS、动态组、RBAC、审计</td><td>企业能力</td><td>取决于购买方案</td></tr><tr><td>Compose Artifact 生成和安装</td><td>官方工具</td><td>使用 <code>gen_docker-compose</code> 和 Update Module</td></tr><tr><td>Artifact 签名和客户端验签</td><td>官方工具</td><td>仍需企业管理私钥和公钥轮换</td></tr><tr><td>CI 上传和创建 Deployment</td><td>流水线脚本</td><td>调用 <code>mender-cli</code> 或 REST API</td></tr><tr><td>前后端健康和版本接口</td><td>业务开发</td><td>Mender 不知道业务是否真正可用</td></tr><tr><td>运行时配置外置</td><td>业务改造</td><td>确保一个镜像能部署到所有地区</td></tr><tr><td>数据库备份、迁移和恢复</td><td>必须自研</td><td>针对实际数据库和业务 schema</td></tr><tr><td>不可逆迁移审批</td><td>流程建设</td><td>不应自动回滚或无人值守执行</td></tr><tr><td>严格同一秒跨地区切换</td><td>自定义协调</td><td>Mender 客户端轮询不保证同时开始</td></tr><tr><td>多套地区 Mender Server 联邦同步</td><td>自定义平台</td><td>Gateway 不提供独立控制面同步</td></tr></tbody></table><p>使用 Mender 的主要价值，是省掉设备注册、轮询协议、制品存储、部署状态机、基础重试和管理 UI。真正需要自己开发的部分集中在业务健康、Compose 约束、数据库和企业发布流程。</p><h2 id="生产上线清单"><a href="#生产上线清单" class="headerlink" title="生产上线清单"></a>生产上线清单</h2><p>正式启用无人值守更新前，至少确认：</p><ul><li>Mender Server 使用固定 Chart 和镜像版本；</li><li>MongoDB、Redis、NATS 和对象存储有明确维护责任；</li><li>MongoDB 与 Artifact 存储已经完成恢复演练；</li><li>总部入口只提供 HTTPS，证书续期有监控；</li><li>每台地区服务器身份唯一；</li><li>地区服务器只需要出站 HTTPS；</li><li>Artifact 强制签名验证；</li><li>私钥不在构建机工作目录、Git 或地区服务器；</li><li>Device Type、架构和 Compose 项目名有统一规范；</li><li>镜像使用不可变版本并保留 digest；</li><li>前端、后端和数据库 schema 使用同一个 <code>releaseId</code>；</li><li>配置、数据库和用户数据位于持久化目录；</li><li>数据库备份和恢复在真实测试库演练通过；</li><li>State Script 幂等并有超时；</li><li>健康检查失败能触发 Compose 和数据库回退；</li><li>保留至少 N-1 版本所需制品和数据恢复点；</li><li>发布按试点、灰度、全量执行；</li><li>有人工暂停、终止、回滚和现场恢复手册；</li><li>日志中不包含密码、私钥、Token 和业务敏感数据。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>使用 Mender 实现中心发布、多地区服务器升级，最合理的职责划分是：</p><ol><li>总部 Mender Server 管理设备、Release、Deployment 和状态；</li><li>总部 CI&#x2F;CD 构建 Compose、打包镜像、生成并签名 Artifact；</li><li>地区 Mender Client 主动轮询、下载、验签和调用 Update Module；</li><li>官方 Compose Update Module 负责容器和 Compose 安装流程；</li><li>企业自己负责业务健康检查、数据库备份、迁移和恢复；</li><li>设备较多或出口受限的地区使用 Mender Gateway 做代理和缓存；</li><li>只有明确要求地区自治时，才部署多套 Mender Server 并开发同步平台。</li></ol><p>Mender 不是把所有升级问题自动解决，而是提供一套可靠的通用分发和设备状态基础。把 Artifact 验签、灰度发布、业务健康和数据库恢复四部分同时做好，才能把“能够远程更新”提升为“可以在生产环境无人值守更新”。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://docs.mender.io/">Mender官方文档</a></li><li><a href="https://docs.mender.io/server-installation/evaluation-with-docker-compose">Mender Server评估环境</a></li><li><a href="https://docs.mender.io/server-installation/production-installation-with-kubernetes">Kubernetes生产部署</a></li><li><a href="https://docs.mender.io/server-installation/production-installation-with-kubernetes/mender-server">Mender Server Helm安装</a></li><li><a href="https://docs.mender.io/client-installation/install-with-debian-package">Mender Client Debian安装</a></li><li><a href="https://docs.mender.io/downloads/device-components">Mender设备端组件下载</a></li><li><a href="https://docs.mender.io/downloads/workstation-tools">Mender工作站工具下载</a></li><li><a href="https://docs.mender.io/artifact-creation/create-a-docker-compose-update-artifact">Docker Compose Artifact创建</a></li><li><a href="https://docs.mender.io/artifact-creation/sign-and-verify">Mender Artifact签名与验证</a></li><li><a href="https://docs.mender.io/artifact-creation/state-scripts">Mender State Scripts</a></li><li><a href="https://docs.mender.io/client-installation/inventory">Mender Inventory</a></li><li><a href="https://docs.mender.io/overview/deployment">Mender Deployment</a></li><li><a href="https://docs.mender.io/server-integration/mender-gateway">Mender Gateway</a></li><li><a href="https://docs.mender.io/server-installation/backup-and-restore">Mender Server备份与恢复</a></li><li><a href="https://docs.mender.io/server-integration/using-the-apis">Mender Server API与CLI</a></li><li><a href="https://docs.mender.io/release-information/supported-releases">Mender支持版本</a></li></ul>]]>
    </content>
    <id>https://blog.wybyte.top/2026/08/22/Mender%E4%B8%AD%E5%BF%83%E5%8F%91%E5%B8%83%E4%B8%8E%E5%A4%9A%E5%9C%B0%E5%8C%BA%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8D%87%E7%BA%A7%E5%AE%8C%E6%95%B4%E5%AE%9E%E8%B7%B5/</id>
    <link href="https://blog.wybyte.top/2026/08/22/Mender%E4%B8%AD%E5%BF%83%E5%8F%91%E5%B8%83%E4%B8%8E%E5%A4%9A%E5%9C%B0%E5%8C%BA%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8D%87%E7%BA%A7%E5%AE%8C%E6%95%B4%E5%AE%9E%E8%B7%B5/"/>
    <published>2026-08-22T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文记录如何使用 Mender 建立一套“总部统一发布版本，各地区服务器主动获取并升级”的系统。</p>
<p>目标场景如下：</p>
<u]]>
    </summary>
    <title>Mender中心发布与多地区服务器升级完整实践</title>
    <updated>2026-09-04T04:23:52.174Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="工具" scheme="https://blog.wybyte.top/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="Codex" scheme="https://blog.wybyte.top/tags/Codex/"/>
    <category term="Python" scheme="https://blog.wybyte.top/tags/Python/"/>
    <category term="Windows" scheme="https://blog.wybyte.top/tags/Windows/"/>
    <content>
      <![CDATA[<p>使用 Codex 一段时间后，偶尔会遇到这样的情况：侧边栏还保留着一条对话，但点击后打不开；日志反复提示某个 <code>rollout</code> 文件不存在；重启 Codex 后，失效记录又重新出现。</p><p>这类问题通常不是项目代码损坏，而是本地索引还指向已经被移动或删除的对话记录文件。本文介绍一个 Python 清理脚本 <code>cleanup_codex_orphan.py</code> 的工作原理和使用方法。文中的盘符、目录名和线程 ID 均为脱敏示例，请替换成自己的实际值。</p><h2 id="问题是怎么产生的"><a href="#问题是怎么产生的" class="headerlink" title="问题是怎么产生的"></a>问题是怎么产生的</h2><p>Codex 本地状态大致分成两部分：</p><ul><li>SQLite 线程索引：默认位于 <code>%USERPROFILE%\.codex\state_5.sqlite</code>，保存线程 ID、标题和 <code>rollout_path</code> 等信息。</li><li>全局 JSON 索引：默认位于 <code>%USERPROFILE%\.codex\.codex-global-state.json</code>，保存侧边栏和客户端关联等状态。</li></ul><p>当 SQLite 仍然保存着一条线程记录，但 <code>rollout_path</code> 指向的 JSONL 文件已经不存在时，就形成了“孤儿索引”。只删除 JSONL 文件本身并不能清理这些索引，反而可能让 Codex 每次启动时继续尝试解析失效路径。</p><p>脚本只清理索引，不会删除项目代码，也不会删除仍然存在的 rollout 文件。默认扫描未归档线程，因为这类记录更可能直接影响侧边栏；需要时可以通过 <code>--include-archived</code> 扩大范围。</p><h2 id="使用前的准备"><a href="#使用前的准备" class="headerlink" title="使用前的准备"></a>使用前的准备</h2><ol><li>先完全退出 Codex 桌面应用，包括托盘中的后台进程。</li><li>准备好 Python 3，并把脚本放到一个自己能确认的目录，例如 <code>D:\tools\codex-cleanup</code>。</li><li>确认当前用户目录下的 <code>.codex</code> 状态文件确实是要处理的那一份。若机器上有多个用户或自定义数据目录，不要直接套用默认路径。</li></ol><blockquote><p><code>--apply</code> 会修改本地状态文件。第一次执行时建议只做 dry-run，确认输出中的线程和路径都可以清理后再写入。</p></blockquote><h2 id="推荐流程：先扫描，再批量清理"><a href="#推荐流程：先扫描，再批量清理" class="headerlink" title="推荐流程：先扫描，再批量清理"></a>推荐流程：先扫描，再批量清理</h2><p>在脚本所在目录执行未归档线程扫描：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python .\cleanup_codex_orphan.py <span class="literal">--scan</span></span><br></pre></td></tr></table></figure><p>脚本会以只读方式打开 SQLite，检查 <code>threads.rollout_path</code> 指向的文件是否存在，并列出发现的线程 ID、缺失路径以及将要移除的 JSON 索引字段。这个命令不会修改任何文件。</p><p>确认结果无误后，执行实际清理：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python .\cleanup_codex_orphan.py <span class="literal">--scan</span> <span class="literal">--apply</span></span><br></pre></td></tr></table></figure><p>如果要把已归档线程也纳入扫描：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">python .\cleanup_codex_orphan.py <span class="literal">--scan</span> <span class="literal">--include-archived</span></span><br><span class="line">python .\cleanup_codex_orphan.py <span class="literal">--scan</span> <span class="literal">--include-archived</span> <span class="literal">--apply</span></span><br></pre></td></tr></table></figure><p>批量写入时，脚本会做两类操作：</p><ul><li>从全局 JSON 中移除与线程 ID 对应的提示历史、权限心跳、线程描述、客户端绑定和项目关联索引。</li><li>从 SQLite 中删除线程的动态工具记录、父子线程边记录以及 <code>threads</code> 主记录。</li></ul><p>删除数据库关联记录后再删除主线程记录，可以避免留下明显的孤立关系。</p><h2 id="只处理一条线程"><a href="#只处理一条线程" class="headerlink" title="只处理一条线程"></a>只处理一条线程</h2><p>如果已经从错误信息中拿到了线程 ID，可以先预览：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">python .\cleanup_codex_orphan.py `</span><br><span class="line">  <span class="literal">--thread-id</span> <span class="string">&quot;aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee&quot;</span></span><br></pre></td></tr></table></figure><p>确认后追加 <code>--apply</code>：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">python .\cleanup_codex_orphan.py `</span><br><span class="line">  <span class="literal">--thread-id</span> <span class="string">&quot;aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee&quot;</span> `</span><br><span class="line">  <span class="literal">--apply</span></span><br></pre></td></tr></table></figure><p>也可以把报错中的 rollout 路径直接传入。路径只要包含标准 UUID，脚本就能提取线程 ID：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">python .\cleanup_codex_orphan.py `</span><br><span class="line">  <span class="literal">--rollout-path</span> <span class="string">&quot;C:\Users\example\.codex\sessions\2026\08\20\rollout-2026-08-20T11-16-11-aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee.jsonl&quot;</span></span><br></pre></td></tr></table></figure><p>如果传入的文件实际上仍然存在，脚本会主动退出，不执行清理，避免误删有效线程。</p><h2 id="日志扫描模式"><a href="#日志扫描模式" class="headerlink" title="日志扫描模式"></a>日志扫描模式</h2><p>某些情况下，SQLite 索引不完整，但 rollout 日志里明确记录了类似下面的错误：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">failed to resolve rollout path `...jsonl`: file does not exist</span><br></pre></td></tr></table></figure><p>此时可以扫描默认 sessions 目录：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python .\cleanup_codex_orphan.py <span class="literal">--scan-logs</span></span><br></pre></td></tr></table></figure><p>日志扫描会识别错误中的缺失路径，提取线程 ID，再预览全局 JSON 中可移除的索引。它是补充手段，完整性通常不如 SQLite 扫描；如果还需要同步删除 SQLite 线程行，优先使用 <code>--scan</code> 或 <code>--thread-id</code> 再复核一次。</p><h2 id="自定义状态文件位置"><a href="#自定义状态文件位置" class="headerlink" title="自定义状态文件位置"></a>自定义状态文件位置</h2><p>如果 Codex 使用了非默认目录，可以显式传入状态库、JSON 状态和 sessions 目录：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">python .\cleanup_codex_orphan.py `</span><br><span class="line">  <span class="literal">--scan</span> `</span><br><span class="line">  <span class="literal">--database</span> <span class="string">&quot;D:\codex-data\state_5.sqlite&quot;</span> `</span><br><span class="line">  <span class="literal">--state-file</span> <span class="string">&quot;D:\codex-data\.codex-global-state.json&quot;</span> `</span><br><span class="line">  <span class="literal">--sessions-dir</span> <span class="string">&quot;D:\codex-data\sessions&quot;</span></span><br></pre></td></tr></table></figure><p>这些参数只覆盖本次执行，不会修改 Codex 的配置。建议先用 <code>--help</code> 查看当前脚本版本支持的完整参数：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python .\cleanup_codex_orphan.py <span class="literal">--help</span></span><br></pre></td></tr></table></figure><h2 id="备份与恢复"><a href="#备份与恢复" class="headerlink" title="备份与恢复"></a>备份与恢复</h2><p>使用 <code>--apply</code> 时，脚本会在原文件旁生成带时间戳的备份，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">state_5.sqlite.before-cleanup-batch-20260821-085643.bak</span><br><span class="line">.codex-global-state.json.before-cleanup-batch-20260821-085643.bak</span><br></pre></td></tr></table></figure><p>如果清理后发现结果不符合预期，先退出 Codex，再用明确的备份文件覆盖当前文件：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Copy-Item</span> `</span><br><span class="line">  <span class="string">&quot;D:\codex-data\state_5.sqlite.before-cleanup-batch-20260821-085643.bak&quot;</span> `</span><br><span class="line">  <span class="string">&quot;D:\codex-data\state_5.sqlite&quot;</span> `</span><br><span class="line">  <span class="literal">-Force</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">Copy-Item</span> `</span><br><span class="line">  <span class="string">&quot;D:\codex-data\.codex-global-state.json.before-cleanup-batch-20260821-085643.bak&quot;</span> `</span><br><span class="line">  <span class="string">&quot;D:\codex-data\.codex-global-state.json&quot;</span> `</span><br><span class="line">  <span class="literal">-Force</span></span><br></pre></td></tr></table></figure><p>恢复后重新启动 Codex，并观察侧边栏和日志是否恢复正常。备份文件确认无用后，再手动删除，避免误删后失去回滚依据。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="database-is-locked"><a href="#database-is-locked" class="headerlink" title="database is locked"></a><code>database is locked</code></h3><p>通常说明 Codex 或其他进程仍在使用 SQLite。确认 Codex 已完全退出后再重试；必要时在任务管理器中检查残留进程。</p><h3 id="扫描结果为-0"><a href="#扫描结果为-0" class="headerlink" title="扫描结果为 0"></a>扫描结果为 0</h3><p>可能是当前没有指向缺失 rollout 的未归档线程、记录已经被清理，或实际使用的是另一个状态库。可以尝试 <code>--include-archived</code>，或者使用 <code>--database</code> 指定正确的 SQLite 文件。</p><h3 id="清理后侧边栏仍有旧记录"><a href="#清理后侧边栏仍有旧记录" class="headerlink" title="清理后侧边栏仍有旧记录"></a>清理后侧边栏仍有旧记录</h3><p>先完全退出并重新启动 Codex，再确认 dry-run 输出。如果记录仍存在，检查是否有多个用户目录、多个状态库，或该条记录来自另一台设备的同步状态。</p><h2 id="边界与风险"><a href="#边界与风险" class="headerlink" title="边界与风险"></a>边界与风险</h2><p>这个脚本清理的是本机 Codex 索引，不等同于账户层面的聊天删除。它也不会恢复已经丢失的 rollout 内容。执行前请保留备份，并只对确认无效的线程使用 <code>--apply</code>。</p><p>如果只是想隐藏一条仍然有效的历史对话，应使用 Codex 自带的归档功能，而不是把它当作“缺失文件”强行清理。归档、删除和本地索引修复是三个不同层次的操作，分清目标后再选择命令，风险最低。</p><h2 id="脚本下载"><a href="#脚本下载" class="headerlink" title="脚本下载"></a>脚本下载</h2><p>脚本文件：<a href="https://imgs.wybyte.top/file/cleanup_codex_orphan.py">https://imgs.wybyte.top/file/cleanup_codex_orphan.py</a></p>]]>
    </content>
    <id>https://blog.wybyte.top/2026/08/21/%E6%B8%85%E7%90%86Codex%E5%AF%B9%E8%AF%9D%E6%AE%8B%E7%95%99%E5%A4%B1%E6%95%88%E7%B4%A2%E5%BC%95/</id>
    <link href="https://blog.wybyte.top/2026/08/21/%E6%B8%85%E7%90%86Codex%E5%AF%B9%E8%AF%9D%E6%AE%8B%E7%95%99%E5%A4%B1%E6%95%88%E7%B4%A2%E5%BC%95/"/>
    <published>2026-08-21T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>使用 Codex 一段时间后，偶尔会遇到这样的情况：侧边栏还保留着一条对话，但点击后打不开；日志反复提示某个 <code>rollout</code> 文件不存在；重启 Codex 后，失效记录又重新出现。</p>
<p>这类问题通常不是项目代码损坏，而是本地索引还指向已经]]>
    </summary>
    <title>清理 Codex 对话残留失效索引：解决侧边栏打不开或反复报错</title>
    <updated>2026-08-21T01:15:18.697Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="系统折腾" scheme="https://blog.wybyte.top/categories/%E7%B3%BB%E7%BB%9F%E6%8A%98%E8%85%BE/"/>
    <category term="macOS" scheme="https://blog.wybyte.top/tags/macOS/"/>
    <category term="VMware" scheme="https://blog.wybyte.top/tags/VMware/"/>
    <category term="OpenCore" scheme="https://blog.wybyte.top/tags/OpenCore/"/>
    <category term="OC4VM" scheme="https://blog.wybyte.top/tags/OC4VM/"/>
    <content>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文记录一次在 Windows 主机上使用 VMware Workstation 和 OC4VM 安装 macOS Sequoia 的完整过程。目标不是追求最新版本，而是在现有硬件和虚拟化软件下尽量兼顾系统版本、兼容性与可维护性。</p><p>本次实际环境如下：</p><table><thead><tr><th>项目</th><th>配置</th></tr></thead><tbody><tr><td>宿主系统</td><td>Windows 11 25H2</td></tr><tr><td>CPU</td><td>Intel，24 线程</td></tr><tr><td>VMware</td><td>Workstation 17.6.4 build-24832109</td></tr><tr><td>引导方案</td><td>OC4VM 3.0.0 Intel 模板</td></tr><tr><td>客体系统</td><td>macOS Sequoia 15</td></tr><tr><td>虚拟机内存</td><td>8 GB</td></tr><tr><td>虚拟 CPU</td><td>2 核</td></tr><tr><td>系统盘</td><td>128 GiB 稀疏 VMDK</td></tr><tr><td>网络</td><td>NAT，e1000e 虚拟网卡</td></tr></tbody></table><p>需要先说明两个边界：</p><ol><li>Apple 的软件许可将 macOS 的安装和虚拟化限制在 Apple 品牌硬件上，在普通 PC 上运行不属于官方许可与支持场景。</li><li>VMware Workstation 在普通 Windows PC 上也不正式支持 macOS 客体系统。即使安装成功，GPU&#x2F;Metal 加速、Apple ID、AirDrop、DRM 视频、休眠等能力仍可能不可用。</li></ol><p>因此，这套环境更适合学习、测试或临时开发，不适合作为主力生产环境。</p><h2 id="为什么选择OC4VM和Sequoia"><a href="#为什么选择OC4VM和Sequoia" class="headerlink" title="为什么选择OC4VM和Sequoia"></a>为什么选择OC4VM和Sequoia</h2><p>过去常见的做法是使用 Unlocker 修改 VMware 程序文件，让 Workstation 显示 macOS 客体类型。这种方案会直接修改 VMware 二进制文件，软件升级后还可能需要重新打补丁。</p><p>本次改用 <a href="https://github.com/DrDonk/OC4VM">OC4VM</a>。它提供预配置的 OpenCore 引导盘和 VMware 模板，不需要修改 VMware 安装目录，并且能够解决 VMware EFI 无法正常进入 macOS Recovery 等问题。OC4VM 3.0.0 明确支持 VMware Workstation 17.6，并包含 Intel 与 AMD 两套模板。</p><p>系统版本选择 macOS Sequoia 15，而没有选择更新的 Tahoe，主要原因是 Tahoe 在没有 Apple 虚拟 GPU 的环境中性能和界面效果受限，声音也需要额外处理。Sequoia 的软件兼容周期较长，同时已经得到 OC4VM 的明确测试。</p><h2 id="安装前准备"><a href="#安装前准备" class="headerlink" title="安装前准备"></a>安装前准备</h2><h3 id="硬件和空间要求"><a href="#硬件和空间要求" class="headerlink" title="硬件和空间要求"></a>硬件和空间要求</h3><p>建议至少准备：</p><ul><li>支持并已开启 VT-x&#x2F;AMD-V 的 64 位 CPU；</li><li>16 GB 主机内存，虚拟机分配 8 GB；</li><li>100 GB 以上可用磁盘空间；</li><li>稳定网络，恢复环境还会下载十几 GB 的完整系统；</li><li>支持 AVX、AVX2、F16C 和 RDRAND 指令的 CPU。</li></ul><h3 id="所需软件"><a href="#所需软件" class="headerlink" title="所需软件"></a>所需软件</h3><p>需要准备三项：</p><ol><li>VMware Workstation 17.6.x；</li><li><a href="https://github.com/DrDonk/OC4VM/releases">OC4VM Release</a>；</li><li>QEMU 中的 <code>qemu-img.exe</code>。</li></ol><p>本机已安装 Chocolatey，因此可以在管理员 PowerShell 中安装 QEMU：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">choco install qemu <span class="literal">-y</span></span><br></pre></td></tr></table></figure><p>安装完成后验证：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">qemu<span class="literal">-img</span> <span class="literal">--version</span></span><br></pre></td></tr></table></figure><p>本次实际使用的路径是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">C:\Program Files\qemu\qemu-img.exe</span><br></pre></td></tr></table></figure><p>OC4VM 3.0.0 解压到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">D:\oc4vm-3.0.0</span><br></pre></td></tr></table></figure><p>解压后应至少存在这些目录：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">config</span><br><span class="line">disks</span><br><span class="line">iso</span><br><span class="line">packages</span><br><span class="line">tools</span><br><span class="line">vmware</span><br></pre></td></tr></table></figure><p>Windows 恢复镜像工具位于：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">D:\oc4vm-3.0.0\tools\windows\recoveryOS.exe</span><br></pre></td></tr></table></figure><h2 id="创建独立虚拟机目录"><a href="#创建独立虚拟机目录" class="headerlink" title="创建独立虚拟机目录"></a>创建独立虚拟机目录</h2><p>现有 VMware 虚拟机都放在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">D:\VMware\Virtual Machines</span><br></pre></td></tr></table></figure><p>为避免和已有 Ubuntu 混在一起，本次单独创建：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">D:\VMware\Virtual Machines\macOS-Sequoia</span><br></pre></td></tr></table></figure><p>然后将 OC4VM 的 Intel 模板内容复制到这个目录：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">D:\oc4vm-3.0.0\vmware\intel</span><br></pre></td></tr></table></figure><p>模板包含：</p><table><thead><tr><th>文件</th><th>用途</th></tr></thead><tbody><tr><td><code>macos.vmx</code></td><td>VMware 虚拟机配置</td></tr><tr><td><code>macos.vmdk</code></td><td>预格式化的 macOS 系统盘</td></tr><tr><td><code>macos.nvram</code></td><td>预配置 NVRAM</td></tr><tr><td><code>opencore.iso</code></td><td>OpenCore 引导镜像</td></tr><tr><td><code>opencore.vmdk</code></td><td>VMware 使用的 OpenCore 磁盘描述文件</td></tr><tr><td><code>macos.plist</code></td><td>VMware Fusion 配置参考</td></tr></tbody></table><p>模板中的 <code>macos.vmdk</code> 是稀疏磁盘，虚拟容量为 128 GiB，刚创建时实际只占约 18 MB，随着系统写入逐步增长。</p><h2 id="下载Apple恢复系统"><a href="#下载Apple恢复系统" class="headerlink" title="下载Apple恢复系统"></a>下载Apple恢复系统</h2><h3 id="推荐的手工方式"><a href="#推荐的手工方式" class="headerlink" title="推荐的手工方式"></a>推荐的手工方式</h3><p>进入 OC4VM 的 Windows 工具目录：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> D:\oc4vm<span class="literal">-3</span>.<span class="number">0.0</span>\tools\windows</span><br></pre></td></tr></table></figure><p>确认 <code>qemu-img</code> 已加入 <code>PATH</code> 后运行：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.\recoveryOS.exe</span><br></pre></td></tr></table></figure><p>工具会显示系统菜单：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">1. Catalina</span><br><span class="line">2. Big Sur</span><br><span class="line">3. Monterey</span><br><span class="line">4. Ventura</span><br><span class="line">5. Sonoma</span><br><span class="line">6. Sequoia</span><br><span class="line">7. Tahoe</span><br></pre></td></tr></table></figure><p>选择 <code>6</code> 下载 Sequoia。下载完成并通过 chunklist 校验后，再选择 <code>1</code> 输出 VMware VMDK。</p><p>该工具从 Apple CDN 下载 <code>BaseSystem.dmg</code> 和对应的 chunklist，不需要第三方 macOS ISO。此次恢复镜像约 847 MiB，转换后的虚拟容量为 3 GiB。</p><h3 id="手工转换备用方案"><a href="#手工转换备用方案" class="headerlink" title="手工转换备用方案"></a>手工转换备用方案</h3><p>如果恢复工具已经完成下载和校验，但没有成功进入格式选择菜单，可以直接转换已下载的 DMG：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&amp; <span class="string">&quot;C:\Program Files\qemu\qemu-img.exe&quot;</span> convert `</span><br><span class="line">  <span class="literal">-p</span> `</span><br><span class="line">  <span class="operator">-f</span> dmg `</span><br><span class="line">  <span class="literal">-O</span> vmdk `</span><br><span class="line">  <span class="literal">-o</span> subformat=monolithicSparse `</span><br><span class="line">  <span class="string">&quot;.\sequoia.dmg&quot;</span> `</span><br><span class="line">  <span class="string">&quot;.\sequoia-recovery.vmdk&quot;</span></span><br></pre></td></tr></table></figure><p>转换完成后检查镜像：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&amp; <span class="string">&quot;C:\Program Files\qemu\qemu-img.exe&quot;</span> check <span class="string">&quot;.\sequoia-recovery.vmdk&quot;</span></span><br><span class="line">&amp; <span class="string">&quot;C:\Program Files\qemu\qemu-img.exe&quot;</span> info <span class="string">&quot;.\sequoia-recovery.vmdk&quot;</span></span><br></pre></td></tr></table></figure><p>本次检查结果为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">No errors were found on the image.</span><br><span class="line">file format: vmdk</span><br><span class="line">virtual size: 3 GiB</span><br><span class="line">create type: monolithicSparse</span><br></pre></td></tr></table></figure><p>确认 VMDK 正常后，<code>.dmg</code>、<code>.chunklist</code> 和构建日志都可以删除，只保留最终的 <code>sequoia-recovery.vmdk</code>。</p><h2 id="配置VMware虚拟机"><a href="#配置VMware虚拟机" class="headerlink" title="配置VMware虚拟机"></a>配置VMware虚拟机</h2><p>将恢复盘放入虚拟机目录：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">D:\VMware\Virtual Machines\macOS-Sequoia\sequoia-recovery.vmdk</span><br></pre></td></tr></table></figure><p>编辑 <code>macos.vmx</code>，修改显示名称：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">displayName = &quot;macOS Sequoia&quot;</span><br></pre></td></tr></table></figure><p>保持系统盘为 <code>sata0:0</code>，再将恢复盘挂载为 <code>sata0:2</code>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">sata0:0.fileName = &quot;macos.vmdk&quot;</span><br><span class="line">sata0:0.present = &quot;TRUE&quot;</span><br><span class="line"></span><br><span class="line">sata0:2.fileName = &quot;sequoia-recovery.vmdk&quot;</span><br><span class="line">sata0:2.present = &quot;TRUE&quot;</span><br><span class="line">sata0:2.redo = &quot;&quot;</span><br></pre></td></tr></table></figure><p>本次模板的其他关键配置如下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">guestOS = &quot;darwin24-64&quot;</span><br><span class="line">numvcpus = &quot;2&quot;</span><br><span class="line">memsize = &quot;8192&quot;</span><br><span class="line">firmware = &quot;efi&quot;</span><br><span class="line">ethernet0.connectionType = &quot;nat&quot;</span><br><span class="line">ethernet0.virtualDev = &quot;e1000e&quot;</span><br><span class="line">smc.present = &quot;FALSE&quot;</span><br></pre></td></tr></table></figure><p>这里的 <code>smc.present = &quot;FALSE&quot;</code> 是正常配置，SMC 由 OC4VM 中的 OpenCore 和 VirtualSMC 提供，不要随意改成 <code>TRUE</code>。</p><p>安装阶段先使用 2 核和 8 GB 内存。系统稳定进入桌面并创建快照后，再尝试提高到 4 核或增加内存。OC4VM 建议 CPU 核数尽量使用 2 的倍数。</p><h2 id="将虚拟机加入VMware"><a href="#将虚拟机加入VMware" class="headerlink" title="将虚拟机加入VMware"></a>将虚拟机加入VMware</h2><p>打开 VMware Workstation，依次选择：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">文件 -&gt; 打开</span><br></pre></td></tr></table></figure><p>选择：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">D:\VMware\Virtual Machines\macOS-Sequoia\macos.vmx</span><br></pre></td></tr></table></figure><p>虚拟机库中会出现 <code>macOS Sequoia</code>。如果首次启动时询问虚拟机是“已移动”还是“已复制”，这是由模板复制产生的新虚拟机，应选择：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">我已复制该虚拟机</span><br></pre></td></tr></table></figure><p>这样 VMware 会生成新的 UUID 和网卡地址，避免和模板或其他虚拟机冲突。</p><h2 id="在Recovery中安装macOS"><a href="#在Recovery中安装macOS" class="headerlink" title="在Recovery中安装macOS"></a>在Recovery中安装macOS</h2><p>点击“开启此虚拟机”，OpenCore 会自动引导到 <code>macOS Base System</code>。进入恢复环境后：</p><ol><li>选择使用语言；</li><li>选择“重新安装 macOS Sequoia”；</li><li>点击“继续”并接受许可协议；</li><li>安装目标选择 <code>Macintosh HD</code>；</li><li>等待恢复环境从 Apple 下载完整安装程序；</li><li>后续会自动重启多次，不要手动改启动盘。</li></ol><h3 id="找不到Macintosh-HD时"><a href="#找不到Macintosh-HD时" class="headerlink" title="找不到Macintosh HD时"></a>找不到Macintosh HD时</h3><p>如果安装器没有显示目标磁盘，返回恢复工具主界面并打开“磁盘工具”：</p><ol><li>在菜单中选择“显示所有设备”；</li><li>找到容量约 128 GB 的虚拟磁盘；</li><li>不要选择容量约 3 GB 的 Recovery 恢复盘；</li><li>抹掉 128 GB 虚拟磁盘；</li><li>名称填写 <code>Macintosh HD</code>；</li><li>格式选择 <code>APFS</code>；</li><li>方案选择 <code>GUID 分区图</code>；</li><li>完成后退出磁盘工具，重新选择“安装 macOS”。</li></ol><p>安装期间网络必须保持连接。恢复环境首先下载完整系统，然后进入 <code>macOS Installer</code>。虚拟机重启时 OpenCore 会自动选择相应安装阶段，最终进入 <code>Macintosh HD</code>。</p><h2 id="首次开机设置"><a href="#首次开机设置" class="headerlink" title="首次开机设置"></a>首次开机设置</h2><p>进入欢迎界面后，建议这样设置：</p><ul><li>选择国家或地区、键盘和语言；</li><li>选择“设置为新的 Mac”；</li><li>辅助功能暂时选择“以后”；</li><li>创建本地用户；</li><li>Apple ID 选择“稍后设置”；</li><li>关闭定位服务并手工选择时区；</li><li>屏幕使用时间选择“稍后设置”；</li><li>是否发送分析数据按个人需要选择。</li></ul><p>OC4VM 上游明确提醒默认身份不适合直接登录 Apple ID。不要在未正确生成并验证机器序列信息前登录承载重要数据的主 Apple ID。</p><p>进入桌面后还应修改系统更新策略：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">系统设置 -&gt; 通用 -&gt; 软件更新 -&gt; 自动更新</span><br></pre></td></tr></table></figure><p>建议只启用自动下载，不要允许系统自动跨大版本升级。这样可以避免虚拟机未经验证就升级到 Tahoe 或后续版本。</p><h2 id="安装VMware-Tools"><a href="#安装VMware-Tools" class="headerlink" title="安装VMware Tools"></a>安装VMware Tools</h2><p>OC4VM 已包含 macOS 可用的 VMware Tools ISO。在 macOS 中可以从 OpenCore 卷访问：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/Volumes/OPENCORE/OC4VM/iso/darwin.iso</span><br></pre></td></tr></table></figure><p>挂载 <code>darwin.iso</code> 后运行其中的 VMware Tools 安装程序。完成后重启 macOS，可以改善：</p><ul><li>鼠标指针跟随；</li><li>动态分辨率；</li><li>剪贴板交互；</li><li>时间同步等基础集成功能。</li></ul><p>需要注意，VMware Tools 不能为普通 PC 上的 macOS 增加 Metal 或完整 GPU 加速。窗口动画、视频处理、模拟器和需要图形加速的软件仍可能很慢。</p><h2 id="安装后的收尾"><a href="#安装后的收尾" class="headerlink" title="安装后的收尾"></a>安装后的收尾</h2><p>确认 macOS 可以从 <code>Macintosh HD</code> 正常启动后，关闭虚拟机，再从 VMware 设置中移除 3 GiB 的恢复盘。也可以在 <code>macos.vmx</code> 中将它设为不启用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sata0:2.present = &quot;FALSE&quot;</span><br></pre></td></tr></table></figure><p>不要删除以下引导文件：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">opencore.iso</span><br><span class="line">opencore.vmdk</span><br><span class="line">macos.nvram</span><br></pre></td></tr></table></figure><p>这台虚拟机仍然依赖 OpenCore 引导，并不是安装完成后就可以移除 OC4VM。</p><p>建议在完成以下事项后创建第一个 VMware 快照：</p><ol><li>成功进入 macOS 桌面；</li><li>VMware Tools 已安装；</li><li>网络与分辨率正常；</li><li>系统自动跨版本升级已关闭；</li><li>虚拟机已正常关机并再次启动验证。</li></ol><h2 id="最终目录结构"><a href="#最终目录结构" class="headerlink" title="最终目录结构"></a>最终目录结构</h2><p>清理临时文件后，本次虚拟机目录为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">D:\VMware\Virtual Machines\macOS-Sequoia</span><br><span class="line">├── macos.nvram</span><br><span class="line">├── macos.plist</span><br><span class="line">├── macos.vmdk</span><br><span class="line">├── macos.vmx</span><br><span class="line">├── opencore.iso</span><br><span class="line">├── opencore.vmdk</span><br><span class="line">└── sequoia-recovery.vmdk</span><br></pre></td></tr></table></figure><p>本次过程中使用过的目录如下：</p><table><thead><tr><th>目录</th><th>用途</th><th>完成后状态</th></tr></thead><tbody><tr><td><code>D:\oc4vm-3.0.0</code></td><td>OC4VM 3.0.0 原始文件</td><td>保留</td></tr><tr><td><code>C:\Program Files\qemu</code></td><td>QEMU 与 <code>qemu-img.exe</code></td><td>保留</td></tr><tr><td><code>D:\VMware\Virtual Machines\macOS-Sequoia</code></td><td>最终虚拟机</td><td>保留</td></tr><tr><td><code>D:\VMware\Virtual Machines\macOS-Sequoia\_recovery-temp</code></td><td>DMG、chunklist 和日志</td><td>已删除</td></tr><tr><td><code>D:\VMs\macOS-Sequoia</code></td><td>中断操作留下的空目录</td><td>已删除</td></tr></tbody></table><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="启动后反复重启"><a href="#启动后反复重启" class="headerlink" title="启动后反复重启"></a>启动后反复重启</h3><p>先检查以下项目：</p><ul><li>使用的是与 CPU 厂商匹配的 Intel&#x2F;AMD 模板；</li><li>CPU 支持 AVX、AVX2、F16C 和 RDRAND；</li><li><code>opencore.vmdk</code> 与 <code>opencore.iso</code> 没有被删除；</li><li>虚拟机没有启用 Secure Boot；</li><li>没有开启虚拟化 CPU 性能计数器；</li><li>安装阶段没有随意增加奇数个 CPU 核心。</li></ul><h3 id="Recovery中没有网络"><a href="#Recovery中没有网络" class="headerlink" title="Recovery中没有网络"></a>Recovery中没有网络</h3><p>Ventura 及之后版本需要兼容的虚拟网卡。OC4VM 模板已经配置：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ethernet0.virtualDev = &quot;e1000e&quot;</span><br><span class="line">ethernet0.connectionType = &quot;nat&quot;</span><br></pre></td></tr></table></figure><p>还应确认 VMware 的 NAT 服务正常运行，并且虚拟机网络适配器勾选了“启动时连接”。</p><h3 id="VMware升级后无法启动"><a href="#VMware升级后无法启动" class="headerlink" title="VMware升级后无法启动"></a>VMware升级后无法启动</h3><p>OC4VM 不修改 VMware 二进制文件，因此不会像传统 Unlocker 那样在每次 VMware 更新后必然重打补丁。但是 VMware 的虚拟硬件行为仍可能变化。升级 VMware、OC4VM 或 macOS 前应完整备份虚拟机目录，并保留一个可回滚快照。</p><h3 id="能否使用Xcode"><a href="#能否使用Xcode" class="headerlink" title="能否使用Xcode"></a>能否使用Xcode</h3><p>基础编译通常可以运行，但由于没有完整 GPU&#x2F;Metal 加速，iOS 模拟器和图形密集型工具的体验可能较差。用于长期开发、签名、发布或正式 CI 时，更适合使用实体 Mac、Mac mini 或合规的云端 Mac。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这次安装的核心不是寻找一个现成的黑苹果 VMDK，而是把每个来源和步骤控制清楚：</p><ol><li>使用 OC4VM 提供 OpenCore 与 VMware 模板；</li><li>通过 OC4VM 工具从 Apple CDN 获取恢复系统；</li><li>使用 <code>qemu-img</code> 生成并校验恢复 VMDK；</li><li>将系统盘、恢复盘和 OpenCore 引导盘集中放在单一虚拟机目录；</li><li>从 Recovery 下载并安装完整的 macOS Sequoia；</li><li>安装后清理临时文件、限制自动升级并创建快照。</li></ol><p>相较于下载来源不明的整合镜像，这种方式步骤更多，但文件来源、虚拟机结构和故障排查路径都更清晰。它仍然无法突破非 Apple 硬件上的图形加速和官方支持限制，但作为一套可复现的学习与测试环境已经足够完整。</p><p>参考资料：</p><ul><li><a href="https://github.com/DrDonk/OC4VM">OC4VM 项目说明</a></li><li><a href="https://github.com/DrDonk/OC4VM/wiki/Before-Installation">OC4VM 安装前准备</a></li><li><a href="https://github.com/DrDonk/OC4VM/wiki/Installing-macOS">OC4VM 安装 macOS</a></li><li><a href="https://developer.apple.com/documentation/virtualization">Apple Virtualization 文档</a></li><li><a href="https://knowledge.broadcom.com/external/article/313785/discontinuation-of-support-for-apple-mac.html">Broadcom 关于 macOS 客体支持的说明</a></li></ul>]]>
    </content>
    <id>https://blog.wybyte.top/2026/08/13/Windows-VMware%E5%AE%89%E8%A3%85macOS-Sequoia%E5%AE%8C%E6%95%B4%E8%AE%B0%E5%BD%95/</id>
    <link href="https://blog.wybyte.top/2026/08/13/Windows-VMware%E5%AE%89%E8%A3%85macOS-Sequoia%E5%AE%8C%E6%95%B4%E8%AE%B0%E5%BD%95/"/>
    <published>2026-08-13T07:20:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文记录一次在 Windows 主机上使用 VMware Workstation 和 OC4VM 安装 macOS Sequoia 的完整过]]>
    </summary>
    <title>Windows下使用VMware安装macOS Sequoia完整记录</title>
    <updated>2026-08-13T07:19:52.923Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="Python" scheme="https://blog.wybyte.top/tags/Python/"/>
    <category term="计算机视觉" scheme="https://blog.wybyte.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89/"/>
    <category term="多模态大模型" scheme="https://blog.wybyte.top/tags/%E5%A4%9A%E6%A8%A1%E6%80%81%E5%A4%A7%E6%A8%A1%E5%9E%8B/"/>
    <category term="视频处理" scheme="https://blog.wybyte.top/tags/%E8%A7%86%E9%A2%91%E5%A4%84%E7%90%86/"/>
    <content>
      <![CDATA[<h2 id="项目背景"><a href="#项目背景" class="headerlink" title="项目背景"></a>项目背景</h2><p>在视频相关的科研课题中，找到数据只是第一步。真正耗时的工作通常是从长视频中定位画面清晰、动作明确、镜头连续的短片段，并把结果整理成后续实验能够直接使用的数据。</p><p>基于目前需求在AI协助下完成了下面的项目，这个项目围绕 Internet Archive 公开资源实现了一套完整处理流程：先采集动漫、动画、短剧等视频的元数据，再自动分析源视频，最终输出 2～15 秒有效片段的起止时间。程序不会批量保存最终视频片段，只保留元数据、原视频链接和时间戳，便于后续按需取用。</p><p>从技术路线来看，它不是单纯的爬虫，也不是端到端的模型训练项目，而是一个“数据采集 + 传统计算机视觉 + 本地多模态大模型”的混合系统。</p><h2 id="整体架构"><a href="#整体架构" class="headerlink" title="整体架构"></a>整体架构</h2><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    flowchart TD    A[Internet Archive 搜索 API] --&gt; B[获取候选视频标识]    B --&gt; C[Metadata API 获取文件与作品信息]    C --&gt; D[清洗、分类、去重]    D --&gt; E[导出 CSV &#x2F; JSON 数据集]    E --&gt; F[按需下载源视频]    F --&gt; G[FFprobe 探测媒体信息]    G --&gt; H[PySceneDetect 检测镜头边界]    H --&gt; I[OpenCV + NumPy 规则粗筛]    I --&gt; J[Qwen3-VL 多模态模型复核]    J --&gt; K[dHash 相似片段去重]    K --&gt; L[输出时间戳、复核表与统计结果]  </pre></div><p>整条管线分为两个相对独立的阶段：</p><ol><li>元数据采集：得到结构化的视频数据集和可访问的原视频链接。</li><li>片段筛选：对视频进行镜头切分、画质分析和语义判断，输出符合条件的片段时间范围。</li></ol><h2 id="使用到的技术"><a href="#使用到的技术" class="headerlink" title="使用到的技术"></a>使用到的技术</h2><table><thead><tr><th>技术</th><th>在项目中的作用</th></tr></thead><tbody><tr><td>Python 3.12</td><td>编写采集、分析、筛选和导出逻辑</td></tr><tr><td>Internet Archive API</td><td>搜索视频并获取作品元数据、文件列表和下载地址</td></tr><tr><td><code>urllib</code></td><td>发起 HTTP 请求、下载视频和调用 Ollama 接口</td></tr><tr><td><code>ThreadPoolExecutor</code></td><td>并发获取多个视频的元数据</td></tr><tr><td>FFprobe</td><td>检查视频流、编码、分辨率、帧率和时长</td></tr><tr><td>PySceneDetect</td><td>使用自适应算法检测镜头切换位置</td></tr><tr><td>OpenCV</td><td>视频解码、定点抽帧、图像缩放和视觉指标计算</td></tr><tr><td>NumPy</td><td>完成均值、中位数、标准差和帧间差分计算</td></tr><tr><td>Qwen3-VL 4B</td><td>判断片段是否具有连续场景、清晰主体和有效动作</td></tr><tr><td>Ollama</td><td>在本地部署和调用视觉模型</td></tr><tr><td>dHash</td><td>生成感知哈希，去除内容相似的候选片段</td></tr><tr><td>CSV &#x2F; JSON &#x2F; JSONL</td><td>保存数据集、结果、统计信息和断点状态</td></tr><tr><td>PowerShell + venv</td><td>完成 Windows 环境初始化和运行封装</td></tr><tr><td><code>unittest</code></td><td>验证分类、切片、时间码、去重和导出逻辑</td></tr></tbody></table><h2 id="第一阶段：采集-Internet-Archive-元数据"><a href="#第一阶段：采集-Internet-Archive-元数据" class="headerlink" title="第一阶段：采集 Internet Archive 元数据"></a>第一阶段：采集 Internet Archive 元数据</h2><h3 id="使用官方-API，而不是解析网页"><a href="#使用官方-API，而不是解析网页" class="headerlink" title="使用官方 API，而不是解析网页"></a>使用官方 API，而不是解析网页</h3><p>项目使用 Internet Archive 的 Advanced Search API 搜索候选资源，再通过 Metadata API 获取每个条目的完整信息。检索条件主要围绕以下字段构造：</p><ul><li><code>mediatype</code>：限制为电影或视频资源；</li><li><code>subject</code>：匹配 anime、animation、short film、short drama 等主题；</li><li><code>title</code> 和 <code>description</code>：补充召回标题或简介中包含相关词汇的资源；</li><li><code>language</code>：优先查找中文相关记录。</li></ul><p>相比直接抓取网页，官方 JSON API 的字段更稳定，也更适合科研数据的批量处理。</p><h3 id="数据清洗与标准化"><a href="#数据清洗与标准化" class="headerlink" title="数据清洗与标准化"></a>数据清洗与标准化</h3><p>Internet Archive 中同一个条目通常包含原始文件、转码文件、缩略图、字幕等多种文件。项目会从文件列表中选择 <code>source=original</code> 的视频；如果存在多个原视频，则优先保留体积最大的一个。</p><p>采集结果统一为 21 个字段，主要包括：</p><ul><li>标题、作者、语言和描述；</li><li>原视频链接和作品详情页链接；</li><li>时长、分辨率、帧率、格式和文件大小；</li><li>字幕链接、许可证地址和许可证状态；</li><li>数据分类、集数信息、唯一标识和采集时间。</li></ul><p>描述中的 HTML 标签会被清理；视频与字幕文件名会进行 URL 编码；中文“第十二集”和英文 <code>S01E12</code>、<code>Episode 12</code> 等形式会被正则表达式转换为统一的集数标识。</p><h3 id="并发、重试与断点续跑"><a href="#并发、重试与断点续跑" class="headerlink" title="并发、重试与断点续跑"></a>并发、重试与断点续跑</h3><p>元数据接口属于网络 I&#x2F;O，因此项目使用线程池并发请求，提高采集速度。面对 HTTP 429、500、502、503、504 或网络超时，会采用指数退避策略重试。</p><p>每获得一条有效记录，程序都会立即追加到 JSONL 检查点，并执行 <code>flush + fsync</code> 强制落盘。即使采集中途关闭，下一次运行也能跳过已经完成的记录。最终 CSV 和 JSON 通过临时文件原子替换生成，避免异常中断留下不完整文件。</p><h2 id="第二阶段：寻找有效视频片段"><a href="#第二阶段：寻找有效视频片段" class="headerlink" title="第二阶段：寻找有效视频片段"></a>第二阶段：寻找有效视频片段</h2><h3 id="FFprobe-探测视频"><a href="#FFprobe-探测视频" class="headerlink" title="FFprobe 探测视频"></a>FFprobe 探测视频</h3><p>Internet Archive 中的视频格式并不统一，可能是 MPEG4、WebM、Matroska、QuickTime 或较早的 MPEG1、MPEG2。视频下载后，程序先调用 FFprobe 检查：</p><ul><li>是否存在可用的视频流；</li><li>视频编码、宽高和平均帧率；</li><li>容器中记录的真实时长。</li></ul><p>这一步可以提前排除损坏文件，也为后面的镜头切分提供可靠的媒体信息。</p><h3 id="PySceneDetect-划分连续镜头"><a href="#PySceneDetect-划分连续镜头" class="headerlink" title="PySceneDetect 划分连续镜头"></a>PySceneDetect 划分连续镜头</h3><p>项目使用 PySceneDetect 的 <code>AdaptiveDetector</code> 检测镜头边界。自适应检测会根据相邻帧的变化寻找转场，相比固定阈值，更能适应亮度和运动差异较大的动画、真人视频。</p><p>检测出的镜头会进一步转换为候选片段：</p><ul><li>少于 2 秒的镜头直接舍弃；</li><li>2～15 秒的镜头直接成为候选；</li><li>超过 15 秒的长镜头切成接近 6 秒的小段。</li></ul><p>这样既满足最终片段的时长约束，也避免把整段长镜头直接交给模型处理。</p><h3 id="OpenCV-与-NumPy-规则粗筛"><a href="#OpenCV-与-NumPy-规则粗筛" class="headerlink" title="OpenCV 与 NumPy 规则粗筛"></a>OpenCV 与 NumPy 规则粗筛</h3><p>如果每个候选都调用视觉模型，速度和显存占用都会很高。因此程序先从每个片段等间隔抽取 6 帧，再计算几项成本较低的视觉指标：</p><table><thead><tr><th>指标</th><th>计算方式</th><th>目的</th></tr></thead><tbody><tr><td>亮度</td><td>灰度图均值</td><td>排除严重过暗的画面</td></tr><tr><td>清晰度</td><td>拉普拉斯方差</td><td>排除明显模糊的画面</td></tr><tr><td>信息量</td><td>灰度标准差</td><td>排除纯色、片头或信息过少的画面</td></tr><tr><td>运动量</td><td>相邻采样帧绝对差均值</td><td>排除接近静止的画面</td></tr><tr><td>时长奖励</td><td>与 6 秒目标时长的距离</td><td>优先选择长度适中的片段</td></tr></tbody></table><p>规则分数满分为 100 分，其中亮度和清晰度各占 25 分，运动量占 20 分，信息量和时长各占 15 分。严重过暗、模糊、近似纯色或静态的候选会在这一阶段被直接拒绝。</p><p>快速版本还会把整段视频按时间划分为若干区域，从每个区域选择最接近 6 秒的候选，默认最多分析 60 个。这样既限制计算量，也能避免候选只集中在视频开头。</p><h3 id="Qwen3-VL-语义复核"><a href="#Qwen3-VL-语义复核" class="headerlink" title="Qwen3-VL 语义复核"></a>Qwen3-VL 语义复核</h3><p>通过规则筛选后，得分最高的少量候选会进入多模态模型。项目使用 Ollama 在本机运行 <code>qwen3-vl:4b</code>，不调用收费云端 API，也不需要把视频数据上传到第三方服务。</p><p>程序把 6 张采样帧压缩为 JPEG 并转成 Base64，通过 Ollama 的 <code>/api/chat</code> 接口提交。提示词要求模型判断：</p><ul><li>人物或主要场景在时间上是否连续；</li><li>是否存在快速混剪、闪回或明显转场；</li><li>主体、动作和画面是否清楚；</li><li>是否为静态图、纯文字或大面积遮挡；</li><li>是否包含对话、走动、转身、开门、取物、打斗等明确动作。</li></ul><p>模型必须返回固定 JSON 结构：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;accepted&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;confidence&quot;</span><span class="punctuation">:</span> <span class="number">0.88</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;score&quot;</span><span class="punctuation">:</span> <span class="number">82</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;reason&quot;</span><span class="punctuation">:</span> <span class="string">&quot;人物动作连续，主体清晰，无明显转场&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>只有模型明确接受、评分不低于 70 且置信度不低于 0.65 的候选，才会进入最终结果。低置信度或字段相互矛盾的结果不会静默丢弃，而是写入人工复核表。</p><h3 id="使用-dHash-去除重复片段"><a href="#使用-dHash-去除重复片段" class="headerlink" title="使用 dHash 去除重复片段"></a>使用 dHash 去除重复片段</h3><p>同一视频中经常存在片头重复、静止特写或高度相似的镜头。程序分别计算候选片段首帧、中间帧和尾帧的 dHash，再通过汉明距离比较视觉签名。</p><p>相似度过高的片段只保留模型评分更高的一条。快速版本还会在全部视频结果之间进行一次全局去重，降低最终数据集中的视觉重复率。</p><h2 id="最终实现效果"><a href="#最终实现效果" class="headerlink" title="最终实现效果"></a>最终实现效果</h2><h3 id="元数据采集结果"><a href="#元数据采集结果" class="headerlink" title="元数据采集结果"></a>元数据采集结果</h3><p>当前项目已经生成 200 条有效视频元数据：</p><table><thead><tr><th>数据分类</th><th align="right">数量</th></tr></thead><tbody><tr><td>动画 <code>animation</code></td><td align="right">104</td></tr><tr><td>动漫 <code>anime</code></td><td align="right">92</td></tr><tr><td>短剧 <code>short_drama</code></td><td align="right">4</td></tr><tr><td>合计</td><td align="right">200</td></tr></tbody></table><p>文件格式覆盖 MPEG4、Matroska、QuickTime、MPEG1、MPEG2 和 WebM，其中 MPEG4 数量最多。每条数据都包含原视频直链，并尽可能保留时长、分辨率、帧率、字幕和许可证信息。</p><h3 id="六种格式抽样实验"><a href="#六种格式抽样实验" class="headerlink" title="六种格式抽样实验"></a>六种格式抽样实验</h3><p>项目对六种视频格式各抽取一条进行验证，实验结果如下：</p><table><thead><tr><th>指标</th><th align="right">结果</th></tr></thead><tbody><tr><td>处理视频</td><td align="right">6 条</td></tr><tr><td>成功选出片段的视频</td><td align="right">5 条</td></tr><tr><td>最终片段</td><td align="right">10 条</td></tr><tr><td>需要人工复核的候选</td><td align="right">7 条</td></tr><tr><td>未得到有效片段的视频</td><td align="right">1 条</td></tr></tbody></table><p>六条视频总时长约 78 分钟，单条视频的镜头数从 7 个到 482 个不等。传统规则先过滤大量低质量候选，再由模型复核排名靠前的片段，证明这套分层策略能够处理不同编码、时长和内容类型的视频。</p><p>第一次抽样中，QuickTime 视频没有通过严格筛选。切换到快速管线，将 252 个候选按时间覆盖缩减为 60 个规则候选，并把模型复核数量调整为 5 个后，最终成功得到 1 条有效片段，处理耗时约 167 秒。</p><h3 id="最终输出"><a href="#最终输出" class="headerlink" title="最终输出"></a>最终输出</h3><p>系统会生成以下结果：</p><ul><li><code>selected_clips.csv</code>：最终通过筛选的片段及起止时间；</li><li><code>review_required.csv</code>：低置信度候选，供人工确认；</li><li><code>failed_videos.csv</code>：下载、解码失败或没有有效片段的视频；</li><li><code>checkpoint.jsonl</code>：视频级断点续跑状态；</li><li><code>summary.json</code>：镜头数、候选数、模型调用数、耗时等统计。</li></ul><p>时间戳统一为 <code>HH:MM:SS.mmm</code> 格式。例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">episode_or_part,start_time,end_time,duration</span><br><span class="line">E12-P01,00:03:12.400,00:03:18.400,00:00:06.000</span><br></pre></td></tr></table></figure><p>CSV 使用 UTF-8 BOM 编码，可以直接用 Excel 打开。程序处理完一条视频后会删除缓存中的源文件，因此磁盘空间不会随着全量任务持续增长。</p><h2 id="工程化设计"><a href="#工程化设计" class="headerlink" title="工程化设计"></a>工程化设计</h2><p>为了让长时间运行的科研任务更稳定，项目还实现了几项工程能力：</p><ul><li>视频下载支持 HTTP Range 断点续传和指数退避重试；</li><li>采集阶段和筛选阶段都有独立的 JSONL 检查点；</li><li>每完成一个视频就重新导出结果，降低中断造成的数据损失；</li><li>支持按格式过滤和“每种格式一条”的小样本验证；</li><li>支持限制规则候选、模型候选和最终片段数量；</li><li>提供 Windows PowerShell 初始化与运行脚本；</li><li>使用本地 Ollama 模型，便于控制成本和数据流向。</li></ul><p>项目现有 29 个单元测试，覆盖元数据分类、集数解析、原视频选择、字幕过滤、片段切分、时间码转换、感知哈希距离、检查点和原子导出等逻辑，当前测试全部通过。</p><h2 id="当前局限与后续方向"><a href="#当前局限与后续方向" class="headerlink" title="当前局限与后续方向"></a>当前局限与后续方向</h2><p>目前已经验证的是系统的可运行性和跨格式处理能力，还不能把抽样结果直接解释为算法精度。要形成更完整的科研结论，后续仍需要：</p><ol><li>建立人工标注的测试集，计算 Precision、Recall、F1、漏选率和误选率。</li><li>对比纯规则、纯视觉模型和混合管线，完成消融实验。</li><li>分析亮度、模糊度、运动量以及模型阈值对结果的影响。</li><li>固定模型版本、硬件环境和运行参数，保证实验可复现。</li><li>引入音频、字幕或更密集的时序特征，弥补稀疏抽帧可能遗漏瞬时转场的问题。</li><li>增加真实网络、视频解码和 Ollama 服务的集成测试。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这个项目把一个宽泛的“从网络视频中寻找有效素材”问题拆成了多个成本逐渐增加的步骤：官方 API 负责获得数据，PySceneDetect 负责找边界，OpenCV 和 NumPy 负责低成本排除明显无效片段，Qwen3-VL 负责语义层面的严格判断，最后通过感知哈希和人工复核保证结果质量。</p><p>这种混合方案的关键不在于堆叠模型，而在于让不同技术处理自己最擅长的问题。规则算法便宜、稳定、可解释，大模型擅长复杂语义判断，两者结合后，既控制了推理成本，也让整个过程更容易调试和复现。</p>]]>
    </content>
    <id>https://blog.wybyte.top/2026/08/13/Internet-Archive%E8%A7%86%E9%A2%91%E9%87%87%E9%9B%86%E4%B8%8E%E6%99%BA%E8%83%BD%E7%89%87%E6%AE%B5%E7%AD%9B%E9%80%89%E5%AE%9E%E8%B7%B5/</id>
    <link href="https://blog.wybyte.top/2026/08/13/Internet-Archive%E8%A7%86%E9%A2%91%E9%87%87%E9%9B%86%E4%B8%8E%E6%99%BA%E8%83%BD%E7%89%87%E6%AE%B5%E7%AD%9B%E9%80%89%E5%AE%9E%E8%B7%B5/"/>
    <published>2026-08-13T03:30:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="项目背景"><a href="#项目背景" class="headerlink" title="项目背景"></a>项目背景</h2><p>在视频相关的科研课题中，找到数据只是第一步。真正耗时的工作通常是从长视频中定位画面清晰、动作明确、镜头连续的短片段，并把结果]]>
    </summary>
    <title>Internet Archive视频采集与智能片段筛选实践</title>
    <updated>2026-08-13T03:10:55.428Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="webhook" scheme="https://blog.wybyte.top/categories/webhook/"/>
    <category term="webhook" scheme="https://blog.wybyte.top/tags/webhook/"/>
    <content>
      <![CDATA[<h2 id="什么是webhook"><a href="#什么是webhook" class="headerlink" title="什么是webhook"></a>什么是webhook</h2><p>Webhook 是一个 API 概念，是微服务 API 的使用范式之一，也被成为反向 API，即前端不主动发送请求，完全由后端推送;</p><p>GtiHub中就存在这种功能，通过创建一个GitHub的webhook可以实现项目的自动部署，issues的自动推送等功能;</p><h2 id="如何创建GitHub的webhook"><a href="#如何创建GitHub的webhook" class="headerlink" title="如何创建GitHub的webhook"></a>如何创建GitHub的webhook</h2><p>首先我们要了解它的工作流程：</p><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    sequenceDiagramGithub Webhook -&gt;&gt; API Service: 发送POST请求，带有特殊Header，以及payloadAPI Service -&gt;&gt; Shell: 子进程启动对应脚本API Service -&gt;&gt; Github Webhook: 返回200状态码，及其他自定义信息  </pre></div><h3 id="编写webhook接口"><a href="#编写webhook接口" class="headerlink" title="编写webhook接口"></a>编写webhook接口</h3><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    flowchart LRA[校验是否是github发送的请求] --&gt;|是| B[执行构建脚本]A --&gt; |不是| C[返回UNAUTHORIZED]  </pre></div><p>此处以nest.js创建的接口为例</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webhook.controller.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123;</span><br><span class="line">  <span class="title class_">Controller</span>,</span><br><span class="line">  <span class="title class_">HttpException</span>,</span><br><span class="line">  <span class="title class_">HttpStatus</span>,</span><br><span class="line">  <span class="title class_">Logger</span>,</span><br><span class="line">  <span class="title class_">Post</span>,</span><br><span class="line">  <span class="title class_">Req</span>,</span><br><span class="line">&#125; <span class="keyword">from</span> <span class="string">&#x27;@nestjs/common&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Request</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;express&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> crypto <span class="keyword">from</span> <span class="string">&#x27;crypto&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">WebhooksService</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./webhooks.service&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ConfigService</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;src/config/config.service&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span>(<span class="string">&#x27;webhooks&#x27;</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">WebhooksController</span> &#123;</span><br><span class="line">  <span class="keyword">private</span> <span class="title class_">Logger</span> = <span class="keyword">new</span> <span class="title class_">Logger</span>(<span class="title class_">WebhooksController</span>.<span class="property">name</span>);</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params">    <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">webhooksService</span>: <span class="title class_">WebhooksService</span>,</span></span><br><span class="line"><span class="params">    <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">config</span>: <span class="title class_">ConfigService</span>,</span></span><br><span class="line"><span class="params">  </span>) &#123;&#125;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 校验github webhook设置的secret的方法</span></span><br><span class="line"><span class="comment">   **/</span></span><br><span class="line">  <span class="keyword">private</span> <span class="title function_">verify_signature</span>(<span class="params"><span class="attr">req</span>: <span class="title class_">Request</span>, <span class="attr">deploy</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!req.<span class="property">headers</span>[<span class="string">&#x27;x-hub-signature-256&#x27;</span>]) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">const</span> signature = crypto</span><br><span class="line">      .<span class="title function_">createHmac</span>(</span><br><span class="line">        <span class="string">&#x27;sha256&#x27;</span>,</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">config</span>[</span><br><span class="line">          <span class="string">`webhookSecret<span class="subst">$&#123;deploy.replace(/^\S/, (s) =&gt; s.toUpperCase())&#125;</span>`</span></span><br><span class="line">        ],</span><br><span class="line">      )</span><br><span class="line">      .<span class="title function_">update</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(req.<span class="property">body</span>))</span><br><span class="line">      .<span class="title function_">digest</span>(<span class="string">&#x27;hex&#x27;</span>);</span><br><span class="line">    <span class="keyword">const</span> trusted = <span class="title class_">Buffer</span>.<span class="title function_">from</span>(<span class="string">`sha256=<span class="subst">$&#123;signature&#125;</span>`</span>, <span class="string">&#x27;ascii&#x27;</span>);</span><br><span class="line">    <span class="keyword">const</span> untrusted = <span class="title class_">Buffer</span>.<span class="title function_">from</span>(</span><br><span class="line">      req.<span class="property">headers</span>[<span class="string">&#x27;x-hub-signature-256&#x27;</span>] <span class="keyword">as</span> <span class="built_in">string</span>,</span><br><span class="line">      <span class="string">&#x27;ascii&#x27;</span>,</span><br><span class="line">    );</span><br><span class="line">    <span class="keyword">return</span> crypto.<span class="title function_">timingSafeEqual</span>(trusted, untrusted);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> <span class="title function_">deployHandler</span>(<span class="params"><span class="attr">req</span>: <span class="title class_">Request</span>, <span class="attr">deploy</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="title function_">verify_signature</span>(req, deploy)) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">Logger</span>.<span class="title function_">error</span>(<span class="string">&#x27;Unauthorized&#x27;</span>);</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">Logger</span>.<span class="title function_">error</span>(<span class="string">`headers: \n<span class="subst">$&#123;<span class="built_in">JSON</span>.stringify(req.headers)&#125;</span>`</span>);</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">Logger</span>.<span class="title function_">error</span>(<span class="string">`body: \n<span class="subst">$&#123;<span class="built_in">JSON</span>.stringify(req.body)&#125;</span>`</span>);</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">HttpException</span>(<span class="string">&#x27;Unauthorized&#x27;</span>, <span class="title class_">HttpStatus</span>.<span class="property">UNAUTHORIZED</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (req.<span class="property">headers</span>[<span class="string">&#x27;x-github-event&#x27;</span>] === <span class="string">&#x27;push&#x27;</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="attr">message</span>: <span class="variable language_">this</span>.<span class="property">webhooksService</span>[<span class="string">`<span class="subst">$&#123;deploy&#125;</span>Deploy`</span>](),</span><br><span class="line">        <span class="attr">statusCode</span>: <span class="number">200</span>,</span><br><span class="line">      &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 这里是github webhook 发送除了push之外请求时的返回，如果有需要自行判断事件类型进行处理</span></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">message</span>: <span class="string">&#x27;webhook no handler&#x27;</span>,</span><br><span class="line">      <span class="attr">statusCode</span>: <span class="number">200</span>,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Post</span>(<span class="string">&#x27;blog&#x27;</span>)</span><br><span class="line">  <span class="title function_">blog</span>(<span class="params"><span class="meta">@Req</span>() <span class="attr">req</span>: <span class="title class_">Request</span></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">deployHandler</span>(req, <span class="string">&#x27;blog&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// webhook.service.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Injectable</span>, <span class="title class_">Logger</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@nestjs/common&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; spawn &#125; <span class="keyword">from</span> <span class="string">&#x27;child_process&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">WebhooksService</span> &#123;</span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="title class_">Logger</span> = <span class="keyword">new</span> <span class="title class_">Logger</span>(<span class="title class_">WebhooksService</span>.<span class="property">name</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">Logger</span>.<span class="title function_">log</span>(<span class="string">&#x27;webhooks create...&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">blogDeploy</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">Logger</span>.<span class="title function_">log</span>(<span class="string">&#x27;deploy init&#x27;</span>);</span><br><span class="line">    <span class="comment">// 启动子进程</span></span><br><span class="line">    <span class="keyword">const</span> deploy = <span class="title function_">spawn</span>(<span class="string">&#x27;sh&#x27;</span>, [<span class="string">&#x27;scripts/blog.deploy.sh&#x27;</span>]); <span class="comment">// 执行构建脚本</span></span><br><span class="line">    deploy.<span class="property">stdout</span>.<span class="title function_">on</span>(<span class="string">&#x27;data&#x27;</span>, <span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">Logger</span>.<span class="title function_">log</span>(data);</span><br><span class="line">    &#125;);</span><br><span class="line">    deploy.<span class="property">stderr</span>.<span class="title function_">on</span>(<span class="string">&#x27;data&#x27;</span>, <span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">Logger</span>.<span class="title function_">log</span>(data);</span><br><span class="line">    &#125;);</span><br><span class="line">    deploy.<span class="property">stdout</span>.<span class="title function_">on</span>(<span class="string">&#x27;end&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">Logger</span>.<span class="title function_">log</span>(<span class="string">&#x27;deploy end&#x27;</span>);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    deploy.<span class="property">stdout</span>.<span class="title function_">on</span>(<span class="string">&#x27;error&#x27;</span>, <span class="function">(<span class="params">err</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">Logger</span>.<span class="title function_">error</span>(err);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="comment">// 我这里不管子进程执行进度，直接返回成功，想同步执行的自己改</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;success&#x27;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="创建github-webhook"><a href="#创建github-webhook" class="headerlink" title="创建github webhook"></a>创建github webhook</h3><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    flowchart LRA[点击settings选项卡] --&gt; B[点击webhooks]B --&gt; C[点击add webhook]C --&gt; D[等待发送ping请求]  </pre></div><p><img src="https://imgs.wybyte.top/file/1693901573334.jpg" alt="github"></p><p><img src="https://imgs.wybyte.top/file/1693902103719.jpg" alt="github"></p><p><img src="https://imgs.wybyte.top/file/1693903362967.jpg" alt="github"></p>]]>
    </content>
    <id>https://blog.wybyte.top/2023/08/31/%E5%88%9B%E5%BB%BAGitHub-webhook/</id>
    <link href="https://blog.wybyte.top/2023/08/31/%E5%88%9B%E5%BB%BAGitHub-webhook/"/>
    <published>2023-08-31T16:40:01.000Z</published>
    <summary>
      <![CDATA[<h2 id="什么是webhook"><a href="#什么是webhook" class="headerlink" title="什么是webhook"></a>什么是webhook</h2><p>Webhook 是一个 API 概念，是微服务 API 的使用范式之一，也被]]>
    </summary>
    <title>创建GitHub webhook</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="组件二次封装" scheme="https://blog.wybyte.top/tags/%E7%BB%84%E4%BB%B6%E4%BA%8C%E6%AC%A1%E5%B0%81%E8%A3%85/"/>
    <category term="suggest组件" scheme="https://blog.wybyte.top/tags/suggest%E7%BB%84%E4%BB%B6/"/>
    <content>
      <![CDATA[<h2 id="远程搜索下拉框封装"><a href="#远程搜索下拉框封装" class="headerlink" title="远程搜索下拉框封装"></a>远程搜索下拉框封装</h2><p>首先要有需求，需求驱使才能让我们打工人创造业务组件</p><h3 id="需求分析"><a href="#需求分析" class="headerlink" title="需求分析"></a>需求分析</h3><p>我们平时做一些管理系统之类的应用时都需要一些关联搜索的下拉选择，所以就需要我们写很多下拉相关的业务组件</p><h3 id="方案选择"><a href="#方案选择" class="headerlink" title="方案选择"></a>方案选择</h3><p>思路呢，我暂时就想得到以下几套方案：</p><ul><li>所有的远程搜索下拉框都写一个组件</li><li>封装一个组件作为通用组件，通过props传参实现不同的搜索</li></ul><p>首先分析第一个方案，每个远程搜索都写一个独立的组件。</p><ul><li>优点：很明显不用考虑别的就实现输入和输出就完事了；</li><li>缺点：就是会制造大量的重复代码，以及很多组件出来；</li></ul><p>第二个方案就是封装一个通用组件，不同的搜索传不同参数。</p><ul><li><p>优点：</p><ul><li>一个组件支撑某些情况下更改起来会相对便捷</li><li>通过配置项实现不同的搜索下拉减少功能重复的组件</li></ul></li><li><p>缺点：</p><ul><li>编写起来需要考虑很多情况，很多奇怪的搜索可能照顾不到</li><li>需要的props可能会相对较多</li></ul></li></ul><h3 id="实现思路"><a href="#实现思路" class="headerlink" title="实现思路"></a>实现思路</h3><p>首先考虑远程搜索组件需要用到的必要的属性(element-plus的二次封装，所以它需要的属性以下不在过多赘述，会进行透传的)</p><p>这种组件实际需要的核心属性只包含以下几种：</p><table><thead><tr><th>props</th><th>说明</th><th>类型</th><th>默认值</th><th>是否必传</th></tr></thead><tbody><tr><td>modelValue</td><td>v-model绑定的值</td><td>Array&#x2F;String&#x2F;Number</td><td>-</td><td>是</td></tr><tr><td>url</td><td>后端提供的suggest接口</td><td>String</td><td>-</td><td>否</td></tr><tr><td>queryKey</td><td>suggest接口需要的联想传参字段名</td><td>String</td><td>-</td><td>否</td></tr><tr><td>selectOptionsKey</td><td>下拉option需要的label和value对应的接口返回的字段</td><td>[String, String]</td><td>[‘code’, ‘desc’]</td><td>否</td></tr><tr><td>multiple</td><td>是否多选</td><td>Boolean</td><td>false</td><td>否</td></tr><tr><td>selectDefaultOptions</td><td>默认的下拉框选项，不需要远程或者需要额外选项的时候可以传这个</td><td>Array</td><td>[]</td><td>否</td></tr></tbody></table><p>但是呢，只包含核心属性明显是不足以应付各种需求的，而且大部分场景都是和form表单一起用所以封一起更省事（写业务经常纠结各种场景判断导致时间不够用，所以还是得会偷懒啊）所以还提供下面一些额外的配置：</p><table><thead><tr><th>props</th><th>说明</th><th>类型</th><th>默认值</th></tr></thead><tbody><tr><td>desc</td><td>可选，可通过v-model绑定，下拉选项的展示的值</td><td>String&#x2F;Array</td><td>-</td></tr><tr><td>label</td><td>form-item的label</td><td>String</td><td>-</td></tr><tr><td>rules</td><td>可选，form-item的rules</td><td>FormItemRule</td><td>-</td></tr><tr><td>prop</td><td>可选，form-item的prop</td><td>String</td><td>-</td></tr><tr><td>otherOptions</td><td>一些select相关的配置，都是可选的</td><td>Object</td><td>-</td></tr></tbody></table><p>otherOptions 配置项</p><table><thead><tr><th>props</th><th>说明</th><th>类型</th><th>默认值</th></tr></thead><tbody><tr><td>placeholder</td><td>占位文字</td><td>String &#x2F; () &#x3D;&gt; String</td><td>`请选择${props.label}`</td></tr><tr><td>remoteMethod</td><td>远程搜索方法，不满足通用的时候，自己定一个搜索方法</td><td>(query: string) &#x3D;&gt; any[]</td><td>-</td></tr><tr><td>labelHandler</td><td>下拉选项的展示值处理</td><td>(option: Record&lt;string, T&gt;) &#x3D;&gt; String</td><td>-</td></tr></tbody></table><h3 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h3><p>首先处理我们的props</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">SuggestProps</span> &#123;</span><br><span class="line">  <span class="attr">desc</span>?: <span class="built_in">string</span> | <span class="built_in">number</span> | <span class="title class_">Array</span>&lt;<span class="built_in">string</span> | <span class="built_in">number</span>&gt; | <span class="literal">undefined</span></span><br><span class="line">  <span class="attr">modelValue</span>: <span class="built_in">string</span> | <span class="built_in">number</span> | <span class="title class_">Array</span>&lt;<span class="built_in">string</span> | <span class="built_in">number</span>&gt;</span><br><span class="line">  <span class="attr">label</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">multiple</span>?: <span class="built_in">boolean</span></span><br><span class="line">  <span class="attr">prop</span>?: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">rules</span>?: <span class="title class_">FormItemRule</span> | <span class="title class_">FormItemRule</span>[]</span><br><span class="line">  <span class="attr">selectDefaultOptions</span>?: <span class="title class_">SelectOptions</span>[]</span><br><span class="line">  <span class="attr">url</span>?: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">queryKey</span>?: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">otherOptions</span>?: <span class="built_in">any</span></span><br><span class="line">  <span class="attr">selectOptionsKey</span>?: [<span class="built_in">string</span>, <span class="built_in">string</span>]</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">const</span> props = <span class="title function_">withDefaults</span>(defineProps&lt;<span class="title class_">SuggestProps</span>&gt;(), &#123;</span><br><span class="line">  <span class="attr">desc</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">  <span class="attr">selectDefaultOptions</span>: <span class="function">() =&gt;</span> [],</span><br><span class="line">  <span class="attr">multiple</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">url</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">  <span class="attr">queryKey</span>: <span class="string">&#x27;name&#x27;</span>,</span><br><span class="line">  <span class="attr">selectOptionsKey</span>: <span class="function">() =&gt;</span> [<span class="string">&#x27;code&#x27;</span>, <span class="string">&#x27;desc&#x27;</span>],</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>由于我目前只用得到双向绑定取值所以就定义了两个触发更新的emit</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">SuggestEmits</span> &#123;</span><br><span class="line">  (<span class="attr">event</span>: <span class="string">&#x27;update:modelValue&#x27;</span>, <span class="attr">e</span>: <span class="built_in">string</span> | <span class="built_in">number</span> | <span class="built_in">any</span>[]): <span class="title function_">void</span></span><br><span class="line">  (<span class="attr">event</span>: <span class="string">&#x27;update:desc&#x27;</span>, <span class="attr">e</span>: <span class="built_in">string</span> | <span class="built_in">number</span> | <span class="built_in">any</span>[] | <span class="literal">undefined</span>): <span class="built_in">void</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">const</span> emit = defineEmits&lt;<span class="title class_">SuggestEmits</span>&gt;();</span><br></pre></td></tr></table></figure><p>根据我们传入的props需要提取出一部分属性给el-select用</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; modelValue, label, rules, selectDefaultOptions, multiple, selectOptionsKey &#125; = <span class="title function_">toRefs</span>(props);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> loading = <span class="title function_">ref</span>(<span class="literal">false</span>);</span><br><span class="line"><span class="keyword">const</span> options = ref&lt;<span class="built_in">any</span>[]&gt;([]);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> placeholder = <span class="title function_">computed</span>(<span class="function">() =&gt;</span> <span class="keyword">typeof</span> props?.<span class="property">otherOptions</span>?.<span class="property">placeholder</span> === <span class="string">&#x27;function&#x27;</span> ? props?.<span class="property">otherOptions</span>?.<span class="title function_">placeholder</span>() : <span class="title function_">t</span>(props?.<span class="property">otherOptions</span>?.<span class="property">placeholder</span> ?? <span class="string">&#x27;&#x27;</span>) || <span class="string">`<span class="subst">$&#123;t(<span class="string">&#x27;message.placeholder.placeSelect&#x27;</span>)&#125;</span><span class="subst">$&#123;t(props.label)&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> suggestOptions = <span class="title function_">reactive</span>(&#123;</span><br><span class="line">  <span class="attr">filterable</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">remote</span>: <span class="literal">true</span>,</span><br><span class="line">  multiple,</span><br><span class="line">  loading,</span><br><span class="line">  ...(<span class="title function_">isReactive</span>(props.<span class="property">otherOptions</span>) ? <span class="title function_">toRefs</span>(props.<span class="property">otherOptions</span>) : props.<span class="property">otherOptions</span>),</span><br><span class="line">  placeholder,</span><br><span class="line">  <span class="attr">clearable</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">remoteMethod</span>: <span class="title function_">async</span> (<span class="attr">query</span>: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (query) &#123;</span><br><span class="line">      loading.<span class="property">value</span> = <span class="literal">true</span>;</span><br><span class="line">      <span class="keyword">if</span> (props.<span class="property">otherOptions</span>?.<span class="property">remoteMethod</span>) &#123;</span><br><span class="line">        options.<span class="property">value</span> = <span class="keyword">await</span> props.<span class="property">otherOptions</span>.<span class="title function_">remoteMethod</span>(query);</span><br><span class="line">        loading.<span class="property">value</span> = <span class="literal">false</span>;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> data = <span class="keyword">await</span> $post(props.<span class="property">url</span>, &#123;</span><br><span class="line">          [props.<span class="property">queryKey</span>]: query,</span><br><span class="line">        &#125;);</span><br><span class="line">        <span class="keyword">if</span> (data?.<span class="property">code</span> === <span class="number">0</span>) &#123;</span><br><span class="line">          options.<span class="property">value</span> = data.<span class="property">info</span>.<span class="property">list</span>;</span><br><span class="line">          loading.<span class="property">value</span> = <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      options.<span class="property">value</span> = [];</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>以及一部分el-option需要的属性</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> options = ref&lt;<span class="built_in">any</span>[]&gt;([]);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">labelView</span> = (<span class="params"><span class="attr">item</span>: <span class="title class_">EmptyObjectType</span></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (props?.<span class="property">otherOptions</span>?.<span class="property">labelHandler</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> props.<span class="property">otherOptions</span>.<span class="title function_">labelHandler</span>(item);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> item[selectOptionsKey.<span class="property">value</span>[<span class="number">1</span>]] ?? <span class="string">&#x27;&#x27;</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>模版代码如下：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">el-form-item</span> <span class="attr">:label</span>=<span class="string">&quot;`$&#123;$t(label)&#125;:`&quot;</span> <span class="attr">:rules</span>=<span class="string">&quot;rules&quot;</span> <span class="attr">:prop</span>=<span class="string">&quot;props.prop&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">el-select</span></span></span><br><span class="line"><span class="tag">      <span class="attr">:model-value</span>=<span class="string">&quot;modelValue&quot;</span></span></span><br><span class="line"><span class="tag">      @<span class="attr">update:model-value</span>=<span class="string">&quot;selectChange&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">v-bind</span>=<span class="string">&quot;suggestOptions&quot;</span></span></span><br><span class="line"><span class="tag">    &gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">el-option</span></span></span><br><span class="line"><span class="tag">        <span class="attr">v-for</span>=<span class="string">&quot;item in options&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">:key</span>=<span class="string">&quot;item[selectOptionsKey[0]]&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">:label</span>=<span class="string">&quot;item[selectOptionsKey[1]]&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">:value</span>=<span class="string">&quot;item[selectOptionsKey[0]]&quot;</span></span></span><br><span class="line"><span class="tag">      &gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-html</span>=<span class="string">&quot;labelView(item)&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">el-option</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">el-select</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">el-form-item</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure><p>需要的属性都处理完后，我们该考虑如何处理选中的值如何向上层传递，如何处理上层传下来的数据回显问题了</p><p>首先我们需要知道我们设计将选中项的code(&#x2F;id)和desc(选项描述)都存为了数组(&#x2F;字符串)，那么我们就需要处理两种情况</p><ul><li>数组，也就是多选的情况，通过filter、配合includes过滤出选中的项</li><li>字符串，单选情况，通过find直接找到选中的项</li></ul><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">selectChange</span> = (<span class="params"><span class="attr">e</span>: <span class="built_in">any</span></span>) =&gt; &#123;</span><br><span class="line">  <span class="title function_">emit</span>(<span class="string">&#x27;update:modelValue&#x27;</span>, e);</span><br><span class="line">  <span class="keyword">if</span> (!multiple.<span class="property">value</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> desc = options.<span class="property">value</span>.<span class="title function_">find</span>(<span class="function"><span class="params">i</span> =&gt;</span> i[selectOptionsKey.<span class="property">value</span>[<span class="number">0</span>]] === e)?.[selectOptionsKey.<span class="property">value</span>[<span class="number">1</span>]];</span><br><span class="line">    <span class="title function_">emit</span>(<span class="string">&#x27;update:desc&#x27;</span>, desc);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> desc = options.<span class="property">value</span>.<span class="title function_">filter</span>(<span class="function"><span class="params">i</span> =&gt;</span> e.<span class="title function_">includes</span>(i[selectOptionsKey.<span class="property">value</span>[<span class="number">0</span>]])).<span class="title function_">map</span>(<span class="function"><span class="params">i</span> =&gt;</span> i[selectOptionsKey.<span class="property">value</span>[<span class="number">1</span>]]);</span><br><span class="line">    <span class="title function_">emit</span>(<span class="string">&#x27;update:desc&#x27;</span>, desc);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>目前来讲这个组件已经可以使用了，但是用起来会发现如果初始化的时候传入了modelValue和desc还是会出现选择框内只有code展示的问题，这个问题就是因为初始化的时候el-option是没有值的，这里用一个一次性的监听去处理下初始选项的问题就好了，如下为解决方案：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> cancelWatch = <span class="title function_">watch</span>(<span class="function">() =&gt;</span> props.<span class="property">desc</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (multiple.<span class="property">value</span> &amp;&amp; props.<span class="property">desc</span> &amp;&amp; <span class="keyword">typeof</span> props.<span class="property">modelValue</span> === <span class="string">&#x27;object&#x27;</span>) &#123;</span><br><span class="line">    props.<span class="property">modelValue</span>.<span class="title function_">forEach</span>(<span class="function">(<span class="params">value, index</span>) =&gt;</span> &#123;</span><br><span class="line">      options.<span class="property">value</span>.<span class="title function_">push</span>(&#123; [selectOptionsKey.<span class="property">value</span>[<span class="number">0</span>]]: value, [selectOptionsKey.<span class="property">value</span>[<span class="number">1</span>]]: (props.<span class="property">desc</span> <span class="keyword">as</span> <span class="built_in">any</span>)?.[index] ?? value &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="title function_">nextTick</span>(<span class="function">() =&gt;</span> <span class="title function_">cancelWatch</span>());</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (props.<span class="property">desc</span>) &#123;</span><br><span class="line">    options.<span class="property">value</span>.<span class="title function_">push</span>(&#123; [selectOptionsKey.<span class="property">value</span>[<span class="number">0</span>]]: props.<span class="property">modelValue</span>, [selectOptionsKey.<span class="property">value</span>[<span class="number">1</span>]]: props.<span class="property">desc</span> &#125;);</span><br><span class="line">    <span class="title function_">nextTick</span>(<span class="function">() =&gt;</span> <span class="title function_">cancelWatch</span>());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;, &#123; <span class="attr">immediate</span>: <span class="literal">true</span> &#125;);</span><br></pre></td></tr></table></figure><p>解决完初始选项和向上传递选中值的问题后，还要完善我们的默认选项的功能，这里通过watch监听这个props属性，如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">watch</span>(selectDefaultOptions, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  options.<span class="property">value</span> = [...options.<span class="property">value</span>, ...selectDefaultOptions.<span class="property">value</span>];</span><br><span class="line">&#125;, &#123; <span class="attr">immediate</span>: <span class="literal">true</span> &#125;);</span><br></pre></td></tr></table></figure><h3 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;el-form-item :label=&quot;`$&#123;$t(label)&#125;:`&quot; :rules=&quot;rules&quot; :prop=&quot;props.prop&quot;&gt;</span><br><span class="line">    &lt;el-select</span><br><span class="line">      :model-value=&quot;modelValue&quot;</span><br><span class="line">      @update:model-value=&quot;selectChange&quot;</span><br><span class="line">      v-bind=&quot;suggestOptions&quot;</span><br><span class="line">    &gt;</span><br><span class="line">      &lt;el-option</span><br><span class="line">        v-for=&quot;item in options&quot;</span><br><span class="line">        :key=&quot;item[selectOptionsKey[0]]&quot;</span><br><span class="line">        :label=&quot;item[selectOptionsKey[1]]&quot;</span><br><span class="line">        :value=&quot;item[selectOptionsKey[0]]&quot;</span><br><span class="line">      &gt;</span><br><span class="line">        &lt;div v-html=&quot;labelView(item)&quot;&gt;&lt;/div&gt;</span><br><span class="line">      &lt;/el-option&gt;</span><br><span class="line">    &lt;/el-select&gt;</span><br><span class="line">  &lt;/el-form-item&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line">&lt;script setup lang=&quot;ts&quot; name=&quot;FrSuggest&quot;&gt;</span><br><span class="line">import &#123; $post &#125; from &#x27;@/plugins/axios&#x27;;</span><br><span class="line">import type &#123; FormItemRule &#125; from &#x27;element-plus&#x27;;</span><br><span class="line">import &#123; isReactive, nextTick, watch, ref, toRefs, reactive, computed &#125; from &#x27;vue&#x27;;</span><br><span class="line">import &#123; useI18n &#125; from &#x27;vue-i18n&#x27;;</span><br><span class="line"></span><br><span class="line">interface SuggestProps &#123;</span><br><span class="line">  desc?: string | number | Array&lt;string | number&gt; | undefined</span><br><span class="line">  modelValue: string | number | Array&lt;string | number&gt;</span><br><span class="line">  label: string</span><br><span class="line">  multiple?: boolean</span><br><span class="line">  prop?: string</span><br><span class="line">  rules?: FormItemRule | FormItemRule[]</span><br><span class="line">  selectDefaultOptions?: SelectOptions[]</span><br><span class="line">  url?: string</span><br><span class="line">  queryKey?: string</span><br><span class="line">  otherOptions?: any</span><br><span class="line">  selectOptionsKey?: [string, string]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">interface SuggestEmits &#123;</span><br><span class="line">  (event: &#x27;update:modelValue&#x27;, e: string | number | any[]): void</span><br><span class="line">  (event: &#x27;update:desc&#x27;, e: string | number | any[] | undefined): void</span><br><span class="line">&#125;</span><br><span class="line">const &#123; t &#125; = useI18n();</span><br><span class="line">const props = withDefaults(defineProps&lt;SuggestProps&gt;(), &#123;</span><br><span class="line">  desc: &#x27;&#x27;,</span><br><span class="line">  selectDefaultOptions: () =&gt; [],</span><br><span class="line">  multiple: false,</span><br><span class="line">  url: &#x27;&#x27;,</span><br><span class="line">  queryKey: &#x27;name&#x27;,</span><br><span class="line">  selectOptionsKey: () =&gt; [&#x27;code&#x27;, &#x27;desc&#x27;],</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">const &#123; modelValue, label, rules, selectDefaultOptions, multiple, selectOptionsKey &#125; = toRefs(props);</span><br><span class="line">const emit = defineEmits&lt;SuggestEmits&gt;();</span><br><span class="line"></span><br><span class="line">const loading = ref(false);</span><br><span class="line">const options = ref&lt;any[]&gt;([]);</span><br><span class="line"></span><br><span class="line">const placeholder = computed(() =&gt; typeof props?.otherOptions?.placeholder === &#x27;function&#x27; ? props?.otherOptions?.placeholder() : t(props?.otherOptions?.placeholder ?? &#x27;&#x27;) || `$&#123;t(&#x27;message.placeholder.placeSelect&#x27;)&#125;$&#123;t(props.label)&#125;`);</span><br><span class="line"></span><br><span class="line">const suggestOptions = reactive(&#123;</span><br><span class="line">  filterable: true,</span><br><span class="line">  remote: true,</span><br><span class="line">  multiple,</span><br><span class="line">  loading,</span><br><span class="line">  ...(isReactive(props.otherOptions) ? toRefs(props.otherOptions) : props.otherOptions),</span><br><span class="line">  placeholder,</span><br><span class="line">  clearable: true,</span><br><span class="line">  remoteMethod: async (query: string) =&gt; &#123;</span><br><span class="line">    if (query) &#123;</span><br><span class="line">      loading.value = true;</span><br><span class="line">      if (props.otherOptions?.remoteMethod) &#123;</span><br><span class="line">        options.value = await props.otherOptions.remoteMethod(query);</span><br><span class="line">        loading.value = false;</span><br><span class="line">      &#125; else &#123;</span><br><span class="line">        const data = await $post(props.url, &#123;</span><br><span class="line">          [props.queryKey]: query,</span><br><span class="line">        &#125;);</span><br><span class="line">        if (data?.code === 0) &#123;</span><br><span class="line">          options.value = data.info.list;</span><br><span class="line">          loading.value = false;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">      options.value = [];</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">const labelView = (item: EmptyObjectType) =&gt; &#123;</span><br><span class="line">  if (props?.otherOptions?.labelHandler) &#123;</span><br><span class="line">    return props.otherOptions.labelHandler(item);</span><br><span class="line">  &#125;</span><br><span class="line">  return item[selectOptionsKey.value[1]] ?? &#x27;&#x27;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">const selectChange = (e: any) =&gt; &#123;</span><br><span class="line">  emit(&#x27;update:modelValue&#x27;, e);</span><br><span class="line">  if (!multiple.value) &#123;</span><br><span class="line">    const desc = options.value.find(i =&gt; i[selectOptionsKey.value[0]] === e)?.[selectOptionsKey.value[1]];</span><br><span class="line">    emit(&#x27;update:desc&#x27;, desc);</span><br><span class="line">  &#125; else &#123;</span><br><span class="line">    const desc = options.value.filter(i =&gt; e.includes(i[selectOptionsKey.value[0]])).map(i =&gt; i[selectOptionsKey.value[1]]);</span><br><span class="line">    emit(&#x27;update:desc&#x27;, desc);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">watch(selectDefaultOptions, () =&gt; &#123;</span><br><span class="line">  options.value = [...options.value, ...selectDefaultOptions.value];</span><br><span class="line">&#125;, &#123; immediate: true &#125;);</span><br><span class="line"></span><br><span class="line">const cancelWatch = watch(() =&gt; props.desc, () =&gt; &#123;</span><br><span class="line">  if (multiple.value &amp;&amp; props.desc &amp;&amp; typeof props.modelValue === &#x27;object&#x27;) &#123;</span><br><span class="line">    props.modelValue.forEach((value, index) =&gt; &#123;</span><br><span class="line">      options.value.push(&#123; [selectOptionsKey.value[0]]: value, [selectOptionsKey.value[1]]: (props.desc as any)?.[index] ?? value &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">    nextTick(() =&gt; cancelWatch());</span><br><span class="line">  &#125; else if (props.desc) &#123;</span><br><span class="line">    options.value.push(&#123; [selectOptionsKey.value[0]]: props.modelValue, [selectOptionsKey.value[1]]: props.desc &#125;);</span><br><span class="line">    nextTick(() =&gt; cancelWatch());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;, &#123; immediate: true &#125;);</span><br><span class="line"></span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><blockquote><p>i18n为国际化翻译用的，不需要的请自行删除</p></blockquote>]]>
    </content>
    <id>https://blog.wybyte.top/2023/08/01/%E8%BF%9C%E7%A8%8B%E6%90%9C%E7%B4%A2%E4%B8%8B%E6%8B%89%E6%A1%86%E5%B0%81%E8%A3%85/</id>
    <link href="https://blog.wybyte.top/2023/08/01/%E8%BF%9C%E7%A8%8B%E6%90%9C%E7%B4%A2%E4%B8%8B%E6%8B%89%E6%A1%86%E5%B0%81%E8%A3%85/"/>
    <published>2023-08-01T13:53:43.000Z</published>
    <summary>
      <![CDATA[<h2 id="远程搜索下拉框封装"><a href="#远程搜索下拉框封装" class="headerlink" title="远程搜索下拉框封装"></a>远程搜索下拉框封装</h2><p>首先要有需求，需求驱使才能让我们打工人创造业务组件</p>
<h3 id="需求分析]]>
    </summary>
    <title>远程搜索下拉框封装</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="Vue" scheme="https://blog.wybyte.top/categories/Vue/"/>
    <category term="Scheduler" scheme="https://blog.wybyte.top/categories/Vue/Scheduler/"/>
    <category term="Vue" scheme="https://blog.wybyte.top/tags/Vue/"/>
    <category term="源码分析" scheme="https://blog.wybyte.top/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <content>
      <![CDATA[<h2 id="Vue-Scheduler"><a href="#Vue-Scheduler" class="headerlink" title="Vue Scheduler"></a>Vue Scheduler</h2><p>Vue中的<code>Scheduler</code>(任务调度系统)，主要责任就是收集Vue在程序执行过程中产生的<code>Jobs</code>(任务)，让每个<code>Job</code>按照固定的顺序执行，避免资源的浪费和程序中的循环依赖导致的死循环。</p><p><code>Vue Scheduler</code>中的核心分为以下几个部分：</p><ul><li>任务队列(queue、pendingPostFlushCbs、activePostFlushCbs)</li><li>任务入队程序(queueJob， queuePostFlushCb)</li><li>任务执行状态(isFlushing、isFlushPending)</li><li>当前正在执行的任务Promise(currentFlushPromise)</li><li>任务执行程序(flushJobs、flushPostFlushCbs、flushPreFlushCbs)</li><li>任务优先级调配程序(comparator)</li></ul><blockquote><p>源码位置：packages&#x2F;runtime-core&#x2F;src&#x2F;scheduler.ts</p></blockquote><h3 id="任务队列"><a href="#任务队列" class="headerlink" title="任务队列"></a>任务队列</h3><p>在<code>Vue Scheduler</code>中的所有任务分为<code>pre</code>、<code>普通任务</code>、<code>post</code>三种显式优先级，以及根据id进行排序的隐式优先级(主要用作将父组件任务前置，确保组件的任务链不会乱序)，如下所示：</p><ul><li><code>pre任务</code>和<code>普通任务</code>放在<code>queue</code>中</li><li><code>post任务</code>放在<code>pendingPostFlushCbs</code>以及<code>activePostFlushCbs</code>中</li></ul><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    flowchart TB  subgraph queue    pre任务    普通任务  end  subgraph pendingPostFlushCbs    post任务  end  subgraph activePostFlushCbs    正在执行的post任务  end  </pre></div><h3 id="任务的添加"><a href="#任务的添加" class="headerlink" title="任务的添加"></a>任务的添加</h3><p>在Vue执行的不同阶段都有任务产生，一部分任务会根据当时的配置立即执行或某些条件下随parent.update执行，另一部分会被添加到任务调度系统中进行统一的调度，以下为Vue中的入队方法：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/runtime-core/src/scheduler.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">queueJob</span>(<span class="params"><span class="attr">job</span>: <span class="title class_">SchedulerJob</span></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (</span><br><span class="line">    !queue.<span class="property">length</span> ||</span><br><span class="line">    !queue.<span class="title function_">includes</span>(</span><br><span class="line">      job,</span><br><span class="line">      isFlushing &amp;&amp; job.<span class="property">allowRecurse</span> ? flushIndex + <span class="number">1</span> : flushIndex</span><br><span class="line">    )</span><br><span class="line">  ) &#123;</span><br><span class="line">    <span class="keyword">if</span> (job.<span class="property">id</span> == <span class="literal">null</span>) &#123;</span><br><span class="line">      queue.<span class="title function_">push</span>(job)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      queue.<span class="title function_">splice</span>(<span class="title function_">findInsertionIndex</span>(job.<span class="property">id</span>), <span class="number">0</span>, job)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="title function_">queueFlush</span>()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">queuePostFlushCb</span>(<span class="params"><span class="attr">cb</span>: <span class="title class_">SchedulerJobs</span></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (!<span class="title function_">isArray</span>(cb)) &#123;</span><br><span class="line">    <span class="keyword">if</span> (</span><br><span class="line">      !activePostFlushCbs ||</span><br><span class="line">      !activePostFlushCbs.<span class="title function_">includes</span>(</span><br><span class="line">        cb,</span><br><span class="line">        cb.<span class="property">allowRecurse</span> ? postFlushIndex + <span class="number">1</span> : postFlushIndex</span><br><span class="line">      )</span><br><span class="line">    ) &#123;</span><br><span class="line">      pendingPostFlushCbs.<span class="title function_">push</span>(cb)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    pendingPostFlushCbs.<span class="title function_">push</span>(...cb)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">queueFlush</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="queueJob"><a href="#queueJob" class="headerlink" title="queueJob"></a>queueJob</h4><p>  在任务进入<code>queueJob</code>时会进行查重的判断，不存在才会将任务push或insert进<code>queue</code>，同时由watch进入的Job是允许递归调用自身的，这时需要开发者自己思考好自己的代码是否会无限执行。</p><ul><li><p>在renderer.ts的<code>setupRenderEffect</code>函数(处理组件渲染的核心函数)中会创建<code>update</code>对象，在<code>update</code>执行时任务会被收集到<code>queue</code>中，如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/runtime-core/src/renderer.ts</span></span><br><span class="line"><span class="comment">// create reactive effect for rendering</span></span><br><span class="line"><span class="keyword">const</span> effect = (instance.<span class="property">effect</span> = <span class="keyword">new</span> <span class="title class_">ReactiveEffect</span>(</span><br><span class="line">  componentUpdateFn,</span><br><span class="line">  <span class="function">() =&gt;</span> <span class="title function_">queueJob</span>(update),</span><br><span class="line">  instance.<span class="property">scope</span> <span class="comment">// track it in component&#x27;s effect scope</span></span><br><span class="line">))</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">update</span>: <span class="title class_">SchedulerJob</span> = (instance.<span class="property">update</span> = <span class="function">() =&gt;</span> effect.<span class="title function_">run</span>())</span><br><span class="line">update.<span class="property">id</span> = instance.<span class="property">uid</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="title function_">update</span>()</span><br></pre></td></tr></table></figure></li><li><p>在this.$forceUpdate()执行时会被放入<code>queue</code>中，如下为Vue在this上拓展的属性其中就包含<code>$forceUpdate</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/runtime-core/src/componentPublicInstance.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="attr">publicPropertiesMap</span>: <span class="title class_">PublicPropertiesMap</span> =</span><br><span class="line">  <span class="comment">// Move PURE marker to new line to workaround compiler discarding it</span></span><br><span class="line">  <span class="comment">// due to type annotation</span></span><br><span class="line">  <span class="comment">/*#__PURE__*/</span> <span class="title function_">extend</span>(<span class="title class_">Object</span>.<span class="title function_">create</span>(<span class="literal">null</span>), &#123;</span><br><span class="line">    <span class="attr">$</span>: <span class="function"><span class="params">i</span> =&gt;</span> i,</span><br><span class="line">    <span class="attr">$el</span>: <span class="function"><span class="params">i</span> =&gt;</span> i.<span class="property">vnode</span>.<span class="property">el</span>,</span><br><span class="line">    <span class="attr">$data</span>: <span class="function"><span class="params">i</span> =&gt;</span> i.<span class="property">data</span>,</span><br><span class="line">    <span class="attr">$props</span>: <span class="function"><span class="params">i</span> =&gt;</span> (__DEV__ ? <span class="title function_">shallowReadonly</span>(i.<span class="property">props</span>) : i.<span class="property">props</span>),</span><br><span class="line">    <span class="attr">$attrs</span>: <span class="function"><span class="params">i</span> =&gt;</span> (__DEV__ ? <span class="title function_">shallowReadonly</span>(i.<span class="property">attrs</span>) : i.<span class="property">attrs</span>),</span><br><span class="line">    <span class="attr">$slots</span>: <span class="function"><span class="params">i</span> =&gt;</span> (__DEV__ ? <span class="title function_">shallowReadonly</span>(i.<span class="property">slots</span>) : i.<span class="property">slots</span>),</span><br><span class="line">    <span class="attr">$refs</span>: <span class="function"><span class="params">i</span> =&gt;</span> (__DEV__ ? <span class="title function_">shallowReadonly</span>(i.<span class="property">refs</span>) : i.<span class="property">refs</span>),</span><br><span class="line">    <span class="attr">$parent</span>: <span class="function"><span class="params">i</span> =&gt;</span> <span class="title function_">getPublicInstance</span>(i.<span class="property">parent</span>),</span><br><span class="line">    <span class="attr">$root</span>: <span class="function"><span class="params">i</span> =&gt;</span> <span class="title function_">getPublicInstance</span>(i.<span class="property">root</span>),</span><br><span class="line">    <span class="attr">$emit</span>: <span class="function"><span class="params">i</span> =&gt;</span> i.<span class="property">emit</span>,</span><br><span class="line">    <span class="attr">$options</span>: <span class="function"><span class="params">i</span> =&gt;</span> (__FEATURE_OPTIONS_API__ ? <span class="title function_">resolveMergedOptions</span>(i) : i.<span class="property">type</span>),</span><br><span class="line">    <span class="attr">$forceUpdate</span>: <span class="function"><span class="params">i</span> =&gt;</span> i.<span class="property">f</span> || (i.<span class="property">f</span> = <span class="function">() =&gt;</span> <span class="title function_">queueJob</span>(i.<span class="property">update</span>)),</span><br><span class="line">    <span class="attr">$nextTick</span>: <span class="function"><span class="params">i</span> =&gt;</span> i.<span class="property">n</span> || (i.<span class="property">n</span> = nextTick.<span class="title function_">bind</span>(i.<span class="property">proxy</span>!)),</span><br><span class="line">    <span class="attr">$watch</span>: <span class="function"><span class="params">i</span> =&gt;</span> (__FEATURE_OPTIONS_API__ ? instanceWatch.<span class="title function_">bind</span>(i) : <span class="variable constant_">NOOP</span>)</span><br><span class="line">  &#125; <span class="keyword">as</span> <span class="title class_">PublicPropertiesMap</span>)</span><br></pre></td></tr></table></figure></li><li><p>异步组件加载时</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/runtime-core/src/apiAsyncComponent.ts</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="title function_">load</span>()</span><br><span class="line">.<span class="title function_">then</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  loaded.<span class="property">value</span> = <span class="literal">true</span></span><br><span class="line">  <span class="keyword">if</span> (instance.<span class="property">parent</span> &amp;&amp; <span class="title function_">isKeepAlive</span>(instance.<span class="property">parent</span>.<span class="property">vnode</span>)) &#123;</span><br><span class="line">    <span class="comment">// parent is keep-alive, force update so the loaded component&#x27;s</span></span><br><span class="line">    <span class="comment">// name is taken into account</span></span><br><span class="line">    <span class="title function_">queueJob</span>(instance.<span class="property">parent</span>.<span class="property">update</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line">.<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">onError</span>(err)</span><br><span class="line">  error.<span class="property">value</span> = err</span><br><span class="line">&#125;)</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure></li><li><p>Watch执行时</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/runtime-core/src/apiWatch.ts</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">let</span> <span class="attr">scheduler</span>: <span class="title class_">EffectScheduler</span></span><br><span class="line"><span class="keyword">if</span> (flush === <span class="string">&#x27;sync&#x27;</span>) &#123;</span><br><span class="line">  scheduler = job <span class="keyword">as</span> <span class="built_in">any</span> <span class="comment">// the scheduler function gets called directly</span></span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> (flush === <span class="string">&#x27;post&#x27;</span>) &#123;</span><br><span class="line">  scheduler = <span class="function">() =&gt;</span> <span class="title function_">queuePostRenderEffect</span>(job, instance &amp;&amp; instance.<span class="property">suspense</span>)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  <span class="comment">// default: &#x27;pre&#x27;</span></span><br><span class="line">  job.<span class="property">pre</span> = <span class="literal">true</span></span><br><span class="line">  <span class="keyword">if</span> (instance) job.<span class="property">id</span> = instance.<span class="property">uid</span></span><br><span class="line">  scheduler = <span class="function">() =&gt;</span> <span class="title function_">queueJob</span>(job)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure></li><li><p>hmr(开发环境热更新)时也会执行<code>queueJob</code></p></li></ul><h4 id="queuePostFlushCb"><a href="#queuePostFlushCb" class="headerlink" title="queuePostFlushCb"></a>queuePostFlushCb</h4><p>  在Job进入<code>queuePostFlushCb</code>时也会进行判断，此时判断的是任务是否在<code>activePostFlushCbs</code>队列中，即是否将要执行，如果不存在即将执行的任务队列则添加到<code>pendingPostFlushCbs</code>队列</p><ul><li><p>在组件的mount、update、unmount等生命周期时都会执行<code>queuePostFlushCb</code>添加后置任务</p><blockquote><p>点太多了，都粘过来有点多余了，具体详见源码: packages&#x2F;runtime-core&#x2F;src&#x2F;renderer.ts</p></blockquote></li><li><p>在<code>Suspense</code>组件中父组件全部加载完之后</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/runtime-core/src/components/Suspense.ts</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="comment">// flush buffered effects</span></span><br><span class="line"><span class="comment">// check if there is a pending parent suspense</span></span><br><span class="line"><span class="keyword">let</span> parent = suspense.<span class="property">parent</span></span><br><span class="line"><span class="keyword">let</span> hasUnresolvedAncestor = <span class="literal">false</span></span><br><span class="line"><span class="keyword">while</span> (parent) &#123;</span><br><span class="line">  <span class="keyword">if</span> (parent.<span class="property">pendingBranch</span>) &#123;</span><br><span class="line">    <span class="comment">// found a pending parent suspense, merge buffered post jobs</span></span><br><span class="line">    <span class="comment">// into that parent</span></span><br><span class="line">    parent.<span class="property">effects</span>.<span class="title function_">push</span>(...effects)</span><br><span class="line">    hasUnresolvedAncestor = <span class="literal">true</span></span><br><span class="line">    <span class="keyword">break</span></span><br><span class="line">  &#125;</span><br><span class="line">  parent = parent.<span class="property">parent</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// no pending parent suspense, flush all jobs</span></span><br><span class="line"><span class="keyword">if</span> (!hasUnresolvedAncestor) &#123;</span><br><span class="line">  <span class="title function_">queuePostFlushCb</span>(effects)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>hmr(热更新之后)</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/runtime-core/src/hmr.ts</span></span><br><span class="line"><span class="comment">// 5. make sure to cleanup dirty hmr components after update</span></span><br><span class="line"><span class="title function_">queuePostFlushCb</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">const</span> instance <span class="keyword">of</span> instances) &#123;</span><br><span class="line">    hmrDirtyComponents.<span class="title function_">delete</span>(</span><br><span class="line">      <span class="title function_">normalizeClassComponent</span>(instance.<span class="property">type</span> <span class="keyword">as</span> <span class="title class_">HMRComponent</span>)</span><br><span class="line">    )</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure></li></ul><blockquote><p>ssr 相关的没列</p></blockquote><h3 id="任务优先级"><a href="#任务优先级" class="headerlink" title="任务优先级"></a>任务优先级</h3><p>Vue中的<code>scheduler</code>通过comparator给<code>queue</code>队列中的<code>pre任务</code>和<code>普通任务</code>进行排序，而<code>pendingPostFlushCbs</code>队列则存放<code>post任务</code>一定在<code>queue</code>队列执行之后才会执行</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/runtime-core/src/scheduler.ts</span></span><br><span class="line"><span class="keyword">const</span> comparator = (<span class="attr">a</span>: <span class="title class_">SchedulerJob</span>, <span class="attr">b</span>: <span class="title class_">SchedulerJob</span>): <span class="function"><span class="params">number</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> diff = <span class="title function_">getId</span>(a) - <span class="title function_">getId</span>(b)</span><br><span class="line">  <span class="comment">// 如果id相同根据是否是pre进行排序，pre优先于普通任务， 如果id不同则根据从小到大排序，确保parent的任务优于children的任务执行</span></span><br><span class="line">  <span class="keyword">if</span> (diff === <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (a.<span class="property">pre</span> &amp;&amp; !b.<span class="property">pre</span>) <span class="keyword">return</span> -<span class="number">1</span></span><br><span class="line">    <span class="keyword">if</span> (b.<span class="property">pre</span> &amp;&amp; !a.<span class="property">pre</span>) <span class="keyword">return</span> <span class="number">1</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> diff</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><div class="mermaid-wrap"><pre class="mermaid-src" data-config="{}" hidden>    flowchart LR  a(pre Job1) --&gt; a2(Job1) --&gt; a3(Job2) --&gt; a4(post Job1)  </pre></div><h3 id="任务执行分析"><a href="#任务执行分析" class="headerlink" title="任务执行分析"></a>任务执行分析</h3><p>任务通过以下三个函数执行：</p><h4 id="flushJobs"><a href="#flushJobs" class="headerlink" title="flushJobs"></a>flushJobs</h4><p>  这个函数最初由<code>queueJob</code>开始触发执行<code>queueFlush</code>，包装一层<code>Promise.resolve</code>后执行<code>flushJobs</code></p><blockquote><p>此处主要分析<code>flushJobs</code>函数，其余源码请看：packages&#x2F;runtime-core&#x2F;src&#x2F;scheduler.ts</p></blockquote>  <figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">flushJobs</span>(<span class="params"><span class="attr">seen</span>?: <span class="title class_">CountMap</span></span>) &#123;</span><br><span class="line">  <span class="comment">// 设置任务状态为Flushing（已完成）</span></span><br><span class="line">  isFlushPending = <span class="literal">false</span></span><br><span class="line">  isFlushing = <span class="literal">true</span></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  queue.<span class="title function_">sort</span>(comparator)</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="comment">// 此处按优先级执行任务Job</span></span><br><span class="line">    <span class="keyword">for</span> (flushIndex = <span class="number">0</span>; flushIndex &lt; queue.<span class="property">length</span>; flushIndex++) &#123;</span><br><span class="line">      <span class="keyword">const</span> job = queue[flushIndex]</span><br><span class="line">      <span class="comment">// 判断Job是否失活，比如任务执行前卸载了组件，就会跳过这个任务</span></span><br><span class="line">      <span class="keyword">if</span> (job &amp;&amp; job.<span class="property">active</span> !== <span class="literal">false</span>) &#123;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">        <span class="comment">// 由统一的错误处理程序执行job，可以统一报错</span></span><br><span class="line">        <span class="comment">// 具体声明详见： packages/runtime-core/src/errorHandling.ts</span></span><br><span class="line">        <span class="title function_">callWithErrorHandling</span>(job, <span class="literal">null</span>, <span class="title class_">ErrorCodes</span>.<span class="property">SCHEDULER</span>)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="comment">// 将标志及队列初始化</span></span><br><span class="line">    flushIndex = <span class="number">0</span></span><br><span class="line">    queue.<span class="property">length</span> = <span class="number">0</span></span><br><span class="line">    <span class="comment">// 执行后置任务</span></span><br><span class="line">    <span class="title function_">flushPostFlushCbs</span>(seen)</span><br><span class="line">    <span class="comment">// 初始化任务状态及任务当前的Promise</span></span><br><span class="line">    isFlushing = <span class="literal">false</span></span><br><span class="line">    currentFlushPromise = <span class="literal">null</span></span><br><span class="line">    <span class="comment">// 如果queue或pendingPostFlushCbs中还存在任务则递归调用当前函数</span></span><br><span class="line">    <span class="keyword">if</span> (queue.<span class="property">length</span> || pendingPostFlushCbs.<span class="property">length</span>) &#123;</span><br><span class="line">      <span class="title function_">flushJobs</span>(seen)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="flushPreFlushCbs"><a href="#flushPreFlushCbs" class="headerlink" title="flushPreFlushCbs"></a>flushPreFlushCbs</h4><blockquote><p>这个函数只会在组件render和updateComponentPreRender时才会执行</p></blockquote>  <figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">flushPreFlushCbs</span>(<span class="params"></span></span><br><span class="line"><span class="params">  <span class="attr">seen</span>?: <span class="title class_">CountMap</span>,</span></span><br><span class="line"><span class="params">  <span class="comment">// if currently flushing, skip the current job itself</span></span></span><br><span class="line"><span class="params">  i = isFlushing ? flushIndex + <span class="number">1</span> : <span class="number">0</span></span></span><br><span class="line"><span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="comment">// 从标识位开始执行，标识位之前的是已经执行过的</span></span><br><span class="line">  <span class="keyword">for</span> (; i &lt; queue.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">const</span> cb = queue[i]</span><br><span class="line">    <span class="comment">// 如果pre属性存在则为前置任务，执行完删除，同时标志位向前挪一位（不挪会导致少执行一个任务）</span></span><br><span class="line">    <span class="keyword">if</span> (cb &amp;&amp; cb.<span class="property">pre</span>) &#123;</span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">      queue.<span class="title function_">splice</span>(i, <span class="number">1</span>)</span><br><span class="line">      i--</span><br><span class="line">      <span class="title function_">cb</span>()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="flushPostFlushCbs"><a href="#flushPostFlushCbs" class="headerlink" title="flushPostFlushCbs"></a>flushPostFlushCbs</h4><p>  这个函数会执行后置任务即<code>post任务</code>，除在<code>flushJobs</code>中执行完<code>queue</code>队列后执行外，在组件render阶段也会被执行</p>  <figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">flushPostFlushCbs</span>(<span class="params"><span class="attr">seen</span>?: <span class="title class_">CountMap</span></span>) &#123;</span><br><span class="line">  <span class="comment">// 判断post任务的队列是否有任务</span></span><br><span class="line">  <span class="keyword">if</span> (pendingPostFlushCbs.<span class="property">length</span>) &#123;</span><br><span class="line">    <span class="comment">// 进行去重，同时清空post队列</span></span><br><span class="line">    <span class="keyword">const</span> deduped = [...<span class="keyword">new</span> <span class="title class_">Set</span>(pendingPostFlushCbs)]</span><br><span class="line">    pendingPostFlushCbs.<span class="property">length</span> = <span class="number">0</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 判断是否存在正在执行的后置任务队列即activePostFlushCbs队列，存在则直接加到队列后边</span></span><br><span class="line">    <span class="keyword">if</span> (activePostFlushCbs) &#123;</span><br><span class="line">      activePostFlushCbs.<span class="title function_">push</span>(...deduped)</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 将pendingPostFlushCbs队列内的任务去重后赋值给正在执行的队列activePostFlushCbs队列</span></span><br><span class="line">    activePostFlushCbs = deduped</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="comment">// 根据id排序，确保parent任务优先执行</span></span><br><span class="line">    activePostFlushCbs.<span class="title function_">sort</span>(<span class="function">(<span class="params">a, b</span>) =&gt;</span> <span class="title function_">getId</span>(a) - <span class="title function_">getId</span>(b))</span><br><span class="line">    <span class="comment">// 按顺序执行任务</span></span><br><span class="line">    <span class="keyword">for</span> (</span><br><span class="line">      postFlushIndex = <span class="number">0</span>;</span><br><span class="line">      postFlushIndex &lt; activePostFlushCbs.<span class="property">length</span>;</span><br><span class="line">      postFlushIndex++</span><br><span class="line">    ) &#123;</span><br><span class="line">      <span class="comment">// ... </span></span><br><span class="line">      activePostFlushCbs[postFlushIndex]()</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 初始化正在执行的后置任务队列activePostFlushCbs</span></span><br><span class="line">    <span class="comment">// 初始化后置任务标志位</span></span><br><span class="line">    activePostFlushCbs = <span class="literal">null</span></span><br><span class="line">    postFlushIndex = <span class="number">0</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="nextTick"><a href="#nextTick" class="headerlink" title="nextTick"></a>nextTick</h3><p>作为Vue中的一个全局API，它的作用是等待下一次 DOM 更新刷新的工具方法.</p><blockquote><p>API介绍： <a href="https://cn.vuejs.org/api/general.html#nexttick">https://cn.vuejs.org/api/general.html#nexttick</a></p></blockquote><p>它同样写在了<code>packages/runtime-core/src/scheduler.ts</code>文件中，它是通过什么才能确保在下一次更新时才执行回调的呢，是不是有这个疑问，下面就是它的源码：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> nextTick&lt;T = <span class="built_in">void</span>, R = <span class="built_in">void</span>&gt;(</span><br><span class="line">  <span class="attr">this</span>: T,</span><br><span class="line">  <span class="attr">fn</span>?: <span class="function">(<span class="params"><span class="attr">this</span>: T</span>) =&gt;</span> R</span><br><span class="line">): <span class="title class_">Promise</span>&lt;<span class="title class_">Awaited</span>&lt;R&gt;&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> p = currentFlushPromise || resolvedPromise</span><br><span class="line">  <span class="keyword">return</span> fn ? p.<span class="title function_">then</span>(<span class="variable language_">this</span> ? fn.<span class="title function_">bind</span>(<span class="variable language_">this</span>) : fn) : p</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它就是在任务调度系统中维护的那个<code>currentFlushPromise</code>当前任务的Promise之后加了个.then的微任务，由于任务队列中的任务都会在currentFlushPromise的第一个.then中执行，所以nextTick才能在那些任务之后执行。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>scheduler的作用就是接收其他的函数丢过来的任务，然后把他们执行掉</p><p>别人只管丢任务给总部，不用管任务什么时候做，就好比我们的排期，产品只管丢给actionView，actionView会安排任务什么时候执行。。</p>]]>
    </content>
    <id>https://blog.wybyte.top/2023/07/25/Vue-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-2/</id>
    <link href="https://blog.wybyte.top/2023/07/25/Vue-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-2/"/>
    <published>2023-07-25T17:33:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="Vue-Scheduler"><a href="#Vue-Scheduler" class="headerlink" title="Vue Scheduler"></a>Vue Scheduler</h2><p>Vue中的<code>Scheduler</code]]>
    </summary>
    <title>Vue-源码解析-2</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="组件二次封装" scheme="https://blog.wybyte.top/tags/%E7%BB%84%E4%BB%B6%E4%BA%8C%E6%AC%A1%E5%B0%81%E8%A3%85/"/>
    <content>
      <![CDATA[<h2 id="基于element-select多分组多级下拉筛选封装"><a href="#基于element-select多分组多级下拉筛选封装" class="headerlink" title="基于element select多分组多级下拉筛选封装"></a>基于element select多分组多级下拉筛选封装</h2><h3 id="实现效果"><a href="#实现效果" class="headerlink" title="实现效果"></a>实现效果</h3><p><img src="https://imgs.wybyte.top/file/1689644065468.jpg" alt="示例"></p><h3 id="具体需求"><a href="#具体需求" class="headerlink" title="具体需求"></a>具体需求</h3><ul><li>这是个多选下拉组件</li><li>组件内包含有<code>分组</code>（<code>分组</code>不可点击）</li><li><code>分组</code>下有<code>二级分类</code></li><li>点击<code>一级分类</code>，选中该分类下所有<code>二级分类</code></li></ul><p>乍一看是挺容易的需求，正当我也以为如此时，转折发生了</p><h3 id="难点出现"><a href="#难点出现" class="headerlink" title="难点出现"></a>难点出现</h3><p>下拉框的源数据结构事例如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&#x27;code&#x27;</span><span class="punctuation">:</span> <span class="number">10</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&#x27;desc&#x27;</span><span class="punctuation">:</span> <span class="string">&#x27;分组1&#x27;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&#x27;children&#x27;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&#x27;code&#x27;</span><span class="punctuation">:</span> <span class="number">1010</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&#x27;desc&#x27;</span><span class="punctuation">:</span> <span class="string">&#x27;类型一级&#x27;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&#x27;sub_type&#x27;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">          <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&#x27;code&#x27;</span><span class="punctuation">:</span> <span class="number">1020</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&#x27;desc&#x27;</span><span class="punctuation">:</span> <span class="string">&#x27;类型二级1&#x27;</span></span><br><span class="line">          <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">]</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&#x27;code&#x27;</span><span class="punctuation">:</span> <span class="number">1011</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&#x27;desc&#x27;</span><span class="punctuation">:</span> <span class="string">&#x27;类型一级2&#x27;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&#x27;sub_type&#x27;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&#x27;code&#x27;</span><span class="punctuation">:</span> <span class="number">20</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&#x27;desc&#x27;</span><span class="punctuation">:</span> <span class="string">&#x27;分组2&#x27;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&#x27;children&#x27;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&#x27;code&#x27;</span><span class="punctuation">:</span> <span class="number">2010</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&#x27;desc&#x27;</span><span class="punctuation">:</span> <span class="string">&#x27;类型一级&#x27;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&#x27;sub_type&#x27;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">          <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&#x27;code&#x27;</span><span class="punctuation">:</span> <span class="number">1020</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&#x27;desc&#x27;</span><span class="punctuation">:</span> <span class="string">&#x27;类型二级1&#x27;</span></span><br><span class="line">          <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">]</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&#x27;code&#x27;</span><span class="punctuation">:</span> <span class="number">2011</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&#x27;desc&#x27;</span><span class="punctuation">:</span> <span class="string">&#x27;类型一级2&#x27;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&#x27;sub_type&#x27;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>看了数据好像也没有感觉出来难在哪，仔细观摩，数据从children开始为<code>分组</code>下的<code>二级分类</code>数据，分为一级和二级，问题点：</p><ul><li><p>一级类型的code不会重复，但是仔细观察发现他居然有可能没有<code>二级分类</code>，这还选个毛啊，实际需求隐式包含了几条规则</p><ul><li>单独一级是可以选的</li><li>有二级的<code>一级分类</code>是不可以单独选中的</li></ul></li><li><p>不同<code>一级分类</code>下的<code>二级分类</code>code是有重复</p></li><li><p>由于上面说的code会重复，所以最后确定后端要的数据结构是个Object类型，如：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> data = &#123;</span><br><span class="line">  <span class="number">1010</span>: [<span class="number">1020</span>], <span class="comment">// 有二级并且选了的就是key是一级code，value是二级数组</span></span><br><span class="line">  <span class="number">1010</span>: [] <span class="comment">// 没有二级只选了一级，value是个空数组</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而我们熟知的select多选的数据结构都是Array类</p></li></ul><h3 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h3><p>了解了需求分析了问题，终于到了该解决问题的时候了，俗话说有困难就解决困难，解决不掉就等着被解决就行了，人和代码有一个能跑就行了</p><h4 id="模版的设计"><a href="#模版的设计" class="headerlink" title="模版的设计"></a>模版的设计</h4><p>如果我们按element的select分组的结构写的话就不可避免的涉及到几个问题：</p><ul><li><code>分组</code>是不可选的</li><li>如果<code>一级分类</code>作为分组，那么如果所有<code>一级分类</code>都没有<code>二级分类</code>，分组会直接不展示，也就是下拉框会变为空的</li><li><code>一级分类</code>是可以单独选择的</li></ul><p>就这几个问题而言，使用默认的select组件带的分组是行不通了</p><p>为了满足以下条件：</p><ul><li><code>分组</code>不可点</li><li>没有<code>二级分类</code>的<code>一级分类</code>可单独选</li><li>有<code>二级分类</code>的<code>一级分类</code>点了就选择当前分类下所有<code>二级分类</code></li></ul><p>思考结构的时候太复杂的结构更不利于维护，尽量往简单了想，索性全部用el-option来处理吧，把多级的结构扁平化直接就一层还好理解，具体实现：</p><ul><li><code>分组</code>可以直接用<code>disabled+class</code>控制不可选就完事了</li><li><code>一级分类</code>分为两种情况：<ul><li>一种是包含<code>二级分类</code>的这个时候<code>一级分类</code>不能单独选中就用<code>div+class模拟</code>稍小一点的option做个区分</li><li>另一种呢是不存在<code>二级分类</code>的<code>一级分类</code>，这个时候它可以单独选中，就用option来实现就行了</li></ul></li><li>二级分类就简单了直接遍历sub_type渲染option就行了</li></ul><p>具体代码：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">el-select</span> <span class="attr">v-model</span>=<span class="string">&quot;selectValue&quot;</span> <span class="attr">:placeholder</span>=<span class="string">&quot;props.placeholder || &#x27;请选择&#x27;&quot;</span> <span class="attr">multiple</span> <span class="attr">clearable</span> <span class="attr">:disabled</span>=<span class="string">&quot;props.disabled&quot;</span> @<span class="attr">change</span>=<span class="string">&quot;handleChange&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">template</span> <span class="attr">v-for</span>=<span class="string">&quot;group in RELATETYPELIST&quot;</span>&gt;</span></span><br><span class="line">      <span class="comment">&lt;!-- 分组 --&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">el-option</span> <span class="attr">class</span>=<span class="string">&quot;el-select-group__title group&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;group.code&quot;</span> <span class="attr">v-if</span>=<span class="string">&quot;group.isTitle&quot;</span> <span class="attr">:label</span>=<span class="string">&quot;`$&#123;group.desc&#125;`&quot;</span> <span class="attr">:value</span>=<span class="string">&quot;group.code&quot;</span> <span class="attr">disabled</span>&gt;</span>&#123;&#123; group.desc &#125;&#125;<span class="tag">&lt;/<span class="name">el-option</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">template</span> <span class="attr">v-else</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 两级，这个充当一级 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span></span></span><br><span class="line"><span class="tag">          <span class="attr">v-if</span>=<span class="string">&quot;group.sub_type &amp;&amp; !!group.sub_type.length&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:key</span>=<span class="string">&quot;group.code&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">class</span>=<span class="string">&quot;el-select-group__title group can-select&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:class</span>=<span class="string">&quot;&#123; &#x27;all-selected&#x27;: groupStatus[group.code] &#125;&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:label</span>=<span class="string">&quot;`$&#123;group.desc&#125;`&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:value</span>=<span class="string">&quot;group.code&quot;</span></span></span><br><span class="line"><span class="tag">          @<span class="attr">click</span>=<span class="string">&quot;handleClickGroup(group)&quot;</span></span></span><br><span class="line"><span class="tag">        &gt;</span>&#123;&#123; group.desc &#125;&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 只有一级的时候 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">el-option</span> <span class="attr">v-else</span> <span class="attr">class</span>=<span class="string">&quot;group can-select&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;group.code&quot;</span> <span class="attr">:label</span>=<span class="string">&quot;`$&#123;group.desc&#125;`&quot;</span> <span class="attr">:value</span>=<span class="string">&quot;group.code&quot;</span>&gt;</span>&#123;&#123; group.desc &#125;&#125;<span class="tag">&lt;/<span class="name">el-option</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 二级分类 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">el-option</span> <span class="attr">class</span>=<span class="string">&quot;pl-30&quot;</span> <span class="attr">v-for</span>=<span class="string">&quot;item in group.sub_type&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;item.code&quot;</span> <span class="attr">:label</span>=<span class="string">&quot;`$&#123;group.desc&#125;-$&#123;item.desc&#125;`&quot;</span> <span class="attr">:value</span>=<span class="string">&quot;item.code&quot;</span>&gt;</span></span><br><span class="line">          &#123;&#123;item.desc&#125;&#125;</span><br><span class="line">        <span class="tag">&lt;/<span class="name">el-option</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">el-select</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="处理源数据"><a href="#处理源数据" class="headerlink" title="处理源数据"></a>处理源数据</h4><p>首先我们确定了后端给我们的数据结构是如下结构：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">BaseData</span> &#123;</span><br><span class="line">  <span class="attr">code</span>: <span class="built_in">number</span></span><br><span class="line">  <span class="attr">desc</span>: <span class="built_in">string</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">LvData</span> <span class="keyword">extends</span> <span class="title class_">BaseData</span> &#123;</span><br><span class="line">  <span class="attr">sub_type</span>: <span class="title class_">BaseData</span>[]</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">GroupData</span> <span class="keyword">extends</span> <span class="title class_">BaseData</span> &#123;</span><br><span class="line">  <span class="attr">children</span>: <span class="title class_">LvData</span>[]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>根据我们想到的<code>template</code>结构，目前这个结构三级嵌套我们没法直接用</p><p>为了达到我们这个尽可能简单的实现的目的来分析这个结构：</p><ul><li>首先<code>分组</code>这一层本来就不能选，而且我们把它扁平化了，起码要与<code>一级分类</code>同级</li><li><code>二级分类</code>总要被遍历的，所以拆开它弊大于利</li></ul><p>所以最终的结构如下:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="variable constant_">RELATETYPELIST</span> = [</span><br><span class="line">  &#123; <span class="attr">code</span>: <span class="number">10</span>, <span class="attr">desc</span>: <span class="string">&#x27;分组1&#x27;</span>, <span class="attr">isTitle</span>: <span class="literal">true</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">code</span>: <span class="number">1010</span>, <span class="attr">desc</span>: <span class="string">&#x27;类型一级&#x27;</span>， <span class="attr">sub_type</span>: [] &#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h5 id="RELATETYPELIST处理"><a href="#RELATETYPELIST处理" class="headerlink" title="RELATETYPELIST处理"></a>RELATETYPELIST处理</h5><p>为了实现点击<code>一级分类</code>快速找到对应的<code>二级分类</code>，并且选中我们还需要一份数据，此处我选择<code>Map类型</code>的数据，好处在于Object类型的数据如果用code做key会被重新排序，为了偷懒（少处理一次数据）,所以在处理渲染数组<code>RELATETYPELIST</code>之前直接造出<code>Map</code>,然后取<code>Map</code>的value就行了，毕竟<code>Map</code>的顺序不会被重排</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 下拉框渲染options的源数据</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">localListToMap</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> localList = [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="string">&#x27;code&#x27;</span>: <span class="number">10</span>,</span><br><span class="line">      <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;分组1&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;children&#x27;</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="string">&#x27;code&#x27;</span>: <span class="number">1010</span>,</span><br><span class="line">          <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型一级&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;sub_type&#x27;</span>: [</span><br><span class="line">            &#123;</span><br><span class="line">              <span class="string">&#x27;code&#x27;</span>: <span class="number">1020</span>,</span><br><span class="line">              <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型二级1&#x27;</span></span><br><span class="line">            &#125;,</span><br><span class="line">          ]</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="string">&#x27;code&#x27;</span>: <span class="number">1011</span>,</span><br><span class="line">          <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型一级2&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;sub_type&#x27;</span>: []</span><br><span class="line">        &#125;,</span><br><span class="line">      ]</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="string">&#x27;code&#x27;</span>: <span class="number">20</span>,</span><br><span class="line">      <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;分组2&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;children&#x27;</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="string">&#x27;code&#x27;</span>: <span class="number">2010</span>,</span><br><span class="line">          <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型一级&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;sub_type&#x27;</span>: [</span><br><span class="line">            &#123;</span><br><span class="line">              <span class="string">&#x27;code&#x27;</span>: <span class="number">1020</span>,</span><br><span class="line">              <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型二级1&#x27;</span></span><br><span class="line">            &#125;,</span><br><span class="line">          ]</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="string">&#x27;code&#x27;</span>: <span class="number">2011</span>,</span><br><span class="line">          <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型一级2&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;sub_type&#x27;</span>: []</span><br><span class="line">        &#125;,</span><br><span class="line">      ]</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">  <span class="keyword">const</span> localListMap = <span class="keyword">new</span> <span class="title class_">Map</span>()</span><br><span class="line">  <span class="comment">// 把最外层的分组跟第一级类型摊平</span></span><br><span class="line">  localList.<span class="title function_">forEach</span>(<span class="function">(<span class="params">parent</span>) =&gt;</span> &#123;</span><br><span class="line">    localListMap.<span class="title function_">set</span>(<span class="string">`<span class="subst">$&#123;parent.code&#125;</span>`</span>, &#123; <span class="attr">code</span>: <span class="string">`<span class="subst">$&#123;parent.code&#125;</span>`</span>, <span class="attr">desc</span>: parent.<span class="property">desc</span>, <span class="attr">isTitle</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">    parent.<span class="property">children</span>.<span class="title function_">forEach</span>(<span class="function"><span class="params">d</span> =&gt;</span> &#123;</span><br><span class="line">      localListMap.<span class="title function_">set</span>(<span class="string">`<span class="subst">$&#123;d.code&#125;</span>`</span>, &#123; ...d, <span class="attr">code</span>: <span class="string">`<span class="subst">$&#123;d.code&#125;</span>`</span>, <span class="attr">sub_type</span>: d.<span class="property">sub_type</span>?.<span class="title function_">map</span>(<span class="function"><span class="params">child</span> =&gt;</span> (&#123; ...child, <span class="attr">code</span>: <span class="string">`<span class="subst">$&#123;d.code&#125;</span>.<span class="subst">$&#123;child.code&#125;</span>`</span> &#125;)) &#125;)</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="comment">// 根据一级code和分组code 组成Map</span></span><br><span class="line">  <span class="keyword">return</span> localListMap</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Map(6) &#123;&#x27;10&#x27; =&gt; &#123;…&#125;, &#x27;1010&#x27; =&gt; &#123;…&#125;, &#x27;1011&#x27; =&gt; &#123;…&#125;, &#x27;20&#x27; =&gt; &#123;…&#125;, &#x27;2010&#x27; =&gt; &#123;…&#125;, …&#125;</span></span><br><span class="line"><span class="comment">// &#123;</span></span><br><span class="line"><span class="comment">//     &quot;key&quot;: &quot;10&quot;,</span></span><br><span class="line"><span class="comment">//     &quot;value&quot;: &#123;</span></span><br><span class="line"><span class="comment">//         &quot;code&quot;: &quot;10&quot;,</span></span><br><span class="line"><span class="comment">//         &quot;desc&quot;: &quot;分组1&quot;,</span></span><br><span class="line"><span class="comment">//         &quot;isTitle&quot;: true</span></span><br><span class="line"><span class="comment">//     &#125;</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br><span class="line"><span class="comment">// &#123;</span></span><br><span class="line"><span class="comment">//     &quot;key&quot;: &quot;1010&quot;,</span></span><br><span class="line"><span class="comment">//     &quot;value&quot;: &#123;</span></span><br><span class="line"><span class="comment">//         &quot;code&quot;: &quot;1010&quot;,</span></span><br><span class="line"><span class="comment">//         &quot;desc&quot;: &quot;类型一级&quot;,</span></span><br><span class="line"><span class="comment">//         &quot;sub_type&quot;: [</span></span><br><span class="line"><span class="comment">//             &#123;</span></span><br><span class="line"><span class="comment">//                 &quot;code&quot;: &quot;1010.1020&quot;,</span></span><br><span class="line"><span class="comment">//                 &quot;desc&quot;: &quot;类型二级1&quot;</span></span><br><span class="line"><span class="comment">//             &#125;</span></span><br><span class="line"><span class="comment">//         ]</span></span><br><span class="line"><span class="comment">//     &#125;</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br></pre></td></tr></table></figure><h5 id="一级类型选中状态处理"><a href="#一级类型选中状态处理" class="headerlink" title="一级类型选中状态处理"></a>一级类型选中状态处理</h5><p>除此之外我们还需要考虑div做<code>一级分类</code>的时候如何准确的加上class，当然在处理<code>Map</code>的时候加上属性控制也可以，但是这样又会让结构变得不再纯粹，所以我使用一个新的对象存状态，这个时候就不需要考虑顺序问题了，只需要存<code>一级类型</code>的code和一个<code>Boolean</code>值就可以了，所以结构如下:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> groupStatus = &#123;</span><br><span class="line">  <span class="number">1010</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="双向绑定的值处理"><a href="#双向绑定的值处理" class="headerlink" title="双向绑定的值处理"></a>双向绑定的值处理</h5><p>下拉框的渲染和一级分类选中的问题解决了，我么该考虑下select组件<code>v-model</code>的数据怎么处理了，这里我才用<code>computed</code>来作为绑定值，具体为啥用<code>computed</code>可以实现看官网吧，下面分析如何写getter和setter：</p><ul><li>输入和输出需要保持数据结构一致</li><li>为了满足后端的要求，我们需要将<code>emit</code>的输出处理成<code>对象</code></li><li>为了满足element组件的要求，我们还需要对getter的输出处理为<code>数组</code></li></ul><p>如下为具体实现:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * select 组件内部绑定的value的getter函数</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">&#123;[key as Stirng]: Array</span>&#125;&#125; value 外部传入的双向绑定对象</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@example</span></span></span><br><span class="line"><span class="comment"> * // value demo</span></span><br><span class="line"><span class="comment"> * const value = &#123;</span></span><br><span class="line"><span class="comment"> *  1010: [1101, 1102, 1103]</span></span><br><span class="line"><span class="comment"> * &#125;</span></span><br><span class="line"><span class="comment"> * const val = getValue(value) // [&#x27;1010.1101&#x27;, &#x27;1010.1102&#x27;, &#x27;1010.1103&#x27;]</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getValue</span> = (<span class="params">value = &#123;&#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> v = []</span><br><span class="line">  <span class="comment">// 将对象转为数组进行处理</span></span><br><span class="line">  <span class="title class_">Object</span>.<span class="title function_">entries</span>(value).<span class="title function_">forEach</span>(<span class="function">(<span class="params">[key, subVal]</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 如果存在二级分类则便利二级分类处理为以 . 链接的字符串满足option的对应关系</span></span><br><span class="line">    <span class="keyword">if</span> (subVal.<span class="property">length</span>) &#123;</span><br><span class="line">      subVal.<span class="title function_">forEach</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> &#123;</span><br><span class="line">        v.<span class="title function_">push</span>(<span class="string">`<span class="subst">$&#123;key&#125;</span>.<span class="subst">$&#123;val&#125;</span>`</span>)</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// 否则直接加进去，为不包含二级分类只有一级分类的option</span></span><br><span class="line">      v.<span class="title function_">push</span>(<span class="string">`<span class="subst">$&#123;key&#125;</span>`</span>)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">return</span> v</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 响应式返回给上一层的数据</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">Array&lt;String&gt;</span>&#125; <span class="variable">value</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span> &#123;<span class="type">&#123;[key as String]: Array</span>&#125;&#125;</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">setValue</span> = (<span class="params">value</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> vMap = <span class="keyword">new</span> <span class="title class_">Map</span>() <span class="comment">// 这里也可以直接用一个空对象进行添加</span></span><br><span class="line">  value.<span class="title function_">forEach</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 将已选的数据每项用 . 拆开表示一级code和二级code</span></span><br><span class="line">    <span class="keyword">const</span> [key, subVal] = val.<span class="title function_">split</span>(<span class="string">&#x27;.&#x27;</span>)</span><br><span class="line">    <span class="comment">// 判断是否存在这个一级code作为key的值，如果vMap用对象的话可以使用hasOwnProperty判断</span></span><br><span class="line">    <span class="keyword">if</span> (!vMap.<span class="title function_">has</span>(key)) &#123;</span><br><span class="line">      vMap.<span class="title function_">set</span>(key, [])</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 如果存在二级code则加入数组里</span></span><br><span class="line">    <span class="keyword">if</span> (subVal) &#123;</span><br><span class="line">      vMap.<span class="title function_">get</span>(key).<span class="title function_">push</span>(subVal)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="comment">// 如果没选中任何值则然后undefined</span></span><br><span class="line">  <span class="keyword">if</span> (!vMap.<span class="property">size</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">undefined</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 返回一个对象，如&#123; 1010: [1020] &#125;</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">fromEntries</span>([...vMap.<span class="title function_">entries</span>()])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 双向绑定的处理</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> selectValue = <span class="title function_">computed</span>(&#123;</span><br><span class="line">  <span class="attr">get</span>: <span class="function">() =&gt;</span> <span class="title function_">getValue</span>(props.<span class="property">value</span>),</span><br><span class="line">  <span class="attr">set</span>: <span class="function">(<span class="params">value</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> v = <span class="title function_">setValue</span>(value)</span><br><span class="line">    <span class="title function_">emit</span>(<span class="string">&#x27;update:value&#x27;</span>, v)</span><br><span class="line">    <span class="title function_">emit</span>(<span class="string">&#x27;changeSelect&#x27;</span>, v)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><blockquote><p>v-model的处理: <a href="https://cn.vuejs.org/guide/components/v-model.html">https://cn.vuejs.org/guide/components/v-model.html</a></p></blockquote><h4 id="事件处理"><a href="#事件处理" class="headerlink" title="事件处理"></a>事件处理</h4><p>至此我们解决了页面渲染的问题</p><p>我们已有的数据如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">useSelectData</span> = (<span class="params">props, emit</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// map结构，一级code做key</span></span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">RELATETYPELISTMAP</span> = <span class="title function_">localListToMap</span>()</span><br><span class="line">  <span class="comment">// 所有一级对应的数据，包含二级数据</span></span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">RELATETYPELIST</span> = [...<span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">values</span>()]</span><br><span class="line">  <span class="comment">// 所有一级分组的选中态， filter过滤掉最外层只展示的分组</span></span><br><span class="line">  <span class="keyword">const</span> groupStatus = <span class="title function_">reactive</span>(</span><br><span class="line">    <span class="title class_">Object</span>.<span class="title function_">fromEntries</span>([...<span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">entries</span>()]</span><br><span class="line">      .<span class="title function_">filter</span>(<span class="function">(<span class="params">[key, val]</span>) =&gt;</span> !val.<span class="property">isTitle</span>)</span><br><span class="line">      .<span class="title function_">map</span>(<span class="function">(<span class="params">[key, val]</span>) =&gt;</span> [key, <span class="literal">false</span>]))</span><br><span class="line">  )</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 双向绑定的处理</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">const</span> selectValue = <span class="title function_">computed</span>(&#123;</span><br><span class="line">    <span class="attr">get</span>: <span class="function">() =&gt;</span> <span class="title function_">getValue</span>(props.<span class="property">value</span>),</span><br><span class="line">    <span class="attr">set</span>: <span class="function">(<span class="params">value</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> v = <span class="title function_">setValue</span>(value)</span><br><span class="line">      <span class="title function_">emit</span>(<span class="string">&#x27;update:value&#x27;</span>, v)</span><br><span class="line">      <span class="title function_">emit</span>(<span class="string">&#x27;changeRelateType&#x27;</span>, v)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="variable constant_">RELATETYPELIST</span>,</span><br><span class="line">    groupStatus,</span><br><span class="line">    selectValue</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们还需要解决的问题是：</p><ul><li>点击<code>二级分类</code>时判断<code>一级分类</code>是否被选中</li><li>点击<code>一级分类</code>时选中所有当前<code>一级分类下</code>的所有<code>二级分类</code></li></ul><h5 id="处理正常点击option"><a href="#处理正常点击option" class="headerlink" title="处理正常点击option"></a>处理正常点击option</h5><p>处理二级分类点击时一级分类是否选中，我们可以直接监听select组件的change事件，具体思路如下：</p><ul><li>当清空select时传入的value会是一个空数组，这个时候把所有的<code>一级分类</code>选中状态的对象都赋值为false</li><li>当选择了某个option时，把已选的数据处理成一个<code>Map</code>或者<code>Object</code>，这样对比源数据和已选数据比较容易，直接比对code一致的<code>一级分类</code>下的<code>二级分类</code>数量就行了，一致的就是全选，此时<code>一级分类</code>变色，不一致就不变</li></ul><p>如下是代码实现：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 选项改变时</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleChange</span> = (<span class="params">value</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 如果清空选项把group的选中态清空</span></span><br><span class="line">  <span class="keyword">if</span> (!value.<span class="property">length</span>) &#123;</span><br><span class="line">    <span class="title class_">Object</span>.<span class="title function_">keys</span>(groupStatus).<span class="title function_">forEach</span>(<span class="function"><span class="params">key</span> =&gt;</span> &#123;</span><br><span class="line">      groupStatus[key] = <span class="literal">false</span></span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> selectedMap = <span class="keyword">new</span> <span class="title class_">Map</span>();</span><br><span class="line">  <span class="comment">// 将已选项处理成一个Map</span></span><br><span class="line">  value.<span class="title function_">forEach</span>(<span class="function">(<span class="params">v</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> [key, val] = v.<span class="title function_">split</span>(<span class="string">&#x27;.&#x27;</span>)</span><br><span class="line">    <span class="keyword">if</span> (!selectedMap.<span class="title function_">has</span>(key)) &#123;</span><br><span class="line">      selectedMap.<span class="title function_">set</span>(key, [])</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (val) &#123;</span><br><span class="line">      selectedMap.<span class="title function_">get</span>(key).<span class="title function_">push</span>(val)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="comment">// 这个selectedMap的结构是</span></span><br><span class="line">  <span class="comment">// Map&lt;&#123;</span></span><br><span class="line">  <span class="comment">//   key: 一级code,</span></span><br><span class="line">  <span class="comment">//   value: [二级code]</span></span><br><span class="line">  <span class="comment">// &#125;&gt;</span></span><br><span class="line">  <span class="comment">// 获取已选项的一级的code</span></span><br><span class="line">  <span class="keyword">const</span> selectKeys = [...selectedMap.<span class="title function_">keys</span>()]</span><br><span class="line">  selectKeys.<span class="title function_">forEach</span>(<span class="function">(<span class="params">key</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 比较已选的二级和源数据二级的长度</span></span><br><span class="line">    <span class="comment">// 长度相等则改变一级分类的选中态</span></span><br><span class="line">    <span class="keyword">if</span> (selectedMap.<span class="title function_">get</span>(key).<span class="property">length</span> === (<span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">get</span>(key)?.<span class="property">sub_type</span> ?? []).<span class="property">length</span>) &#123;</span><br><span class="line">      groupStatus[key] = <span class="literal">true</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      groupStatus[key] = <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="处理点击一级分类"><a href="#处理点击一级分类" class="headerlink" title="处理点击一级分类"></a>处理点击一级分类</h5><p>当点击<code>一级分类</code>的时候需要选中它下面的所有<code>二级分类</code></p><p>点击的时候判断点击的这个<code>一级分类</code>是否已经全选，分两种情况处理具体实现思路如下：</p><ul><li>如果已经全选则取消全选，同时删除他下面<code>二级分类</code>的选中态</li><li>如果不是全选，则把当前<code>一级分类</code>下面的没有被选择的<code>二级分类</code>加到v-model的数据里，同时<code>一级分类</code>状态切换为已选</li></ul><p>具体代码实现如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 点击一级分类时</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">*</span>&#125; <span class="variable">group</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleClickGroup</span> = (<span class="params">group</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 获取子项, 如果不存在二级分类就把当前的一级分类拿出来</span></span><br><span class="line">  <span class="keyword">const</span> childrens = !<span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">get</span>(group.<span class="property">code</span>).<span class="property">sub_type</span>?.<span class="property">length</span> ? [group] : <span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">get</span>(group.<span class="property">code</span>).<span class="property">sub_type</span></span><br><span class="line">  <span class="comment">// 获取子项code</span></span><br><span class="line">  <span class="keyword">const</span> childrensCode = childrens.<span class="title function_">map</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val.<span class="property">code</span>)</span><br><span class="line">  <span class="comment">// 获取已选的code</span></span><br><span class="line">  <span class="keyword">const</span> selected = <span class="keyword">new</span> <span class="title class_">Set</span>(selectValue.<span class="property">value</span>)</span><br><span class="line">  <span class="comment">// 如果当前组已经是全选了，则删除已选的</span></span><br><span class="line">  <span class="keyword">if</span> (groupStatus[group.<span class="property">code</span>]) &#123;</span><br><span class="line">    <span class="comment">// 删除已选的里面包含的子项</span></span><br><span class="line">    childrensCode.<span class="title function_">forEach</span>(<span class="function"><span class="params">code</span> =&gt;</span> &#123;</span><br><span class="line">      selected.<span class="title function_">delete</span>(code)</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="comment">// 重新给select赋值</span></span><br><span class="line">    selectValue.<span class="property">value</span> = [...selected]</span><br><span class="line">    <span class="comment">// 改变当前组的选中态</span></span><br><span class="line">    groupStatus[group.<span class="property">code</span>] = <span class="literal">false</span></span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 往里塞当前组的code</span></span><br><span class="line">  childrensCode.<span class="title function_">forEach</span>(<span class="function"><span class="params">code</span> =&gt;</span> &#123;</span><br><span class="line">    selected.<span class="title function_">add</span>(code)</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="comment">// 重新给select赋值</span></span><br><span class="line">  selectValue.<span class="property">value</span> = [...selected]</span><br><span class="line">  <span class="comment">// 改变当前组的选中态</span></span><br><span class="line">  groupStatus[group.<span class="property">code</span>] = <span class="literal">true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// useSelectData</span></span><br><span class="line"><span class="keyword">import</span> &#123; computed, reactive &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * props类型</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> propsType = &#123;</span><br><span class="line">  <span class="attr">value</span>: &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="title class_">Object</span>,</span><br><span class="line">    <span class="attr">required</span>: <span class="literal">true</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">disabled</span>: &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="title class_">Boolean</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">placeholder</span>: &#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="title class_">String</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * emit的类型</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> emitType = [<span class="string">&#x27;update:value&#x27;</span>, <span class="string">&#x27;changeRelateType&#x27;</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * relateType select 组件内部绑定的value的getter函数</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">&#123;[key as Stirng]: Array</span>&#125;&#125; value 外部传入的双向绑定对象</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@example</span></span></span><br><span class="line"><span class="comment"> * // value demo</span></span><br><span class="line"><span class="comment"> * const value = &#123;</span></span><br><span class="line"><span class="comment"> *  1010: [1101, 1102, 1103]</span></span><br><span class="line"><span class="comment"> * &#125;</span></span><br><span class="line"><span class="comment"> * const val = getValue(value) // [&#x27;1010.1101&#x27;, &#x27;1010.1102&#x27;, &#x27;1010.1103&#x27;]</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getValue</span> = (<span class="params">value = &#123;&#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> v = []</span><br><span class="line">  <span class="title class_">Object</span>.<span class="title function_">entries</span>(value).<span class="title function_">forEach</span>(<span class="function">(<span class="params">[key, subVal]</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (subVal.<span class="property">length</span>) &#123;</span><br><span class="line">      subVal.<span class="title function_">forEach</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> &#123;</span><br><span class="line">        v.<span class="title function_">push</span>(<span class="string">`<span class="subst">$&#123;key&#125;</span>.<span class="subst">$&#123;val&#125;</span>`</span>)</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      v.<span class="title function_">push</span>(<span class="string">`<span class="subst">$&#123;key&#125;</span>`</span>)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">return</span> v</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 响应式返回给上一层的数据</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">Array&lt;String&gt;</span>&#125; <span class="variable">value</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span> &#123;<span class="type">&#123;[key as String]: Array</span>&#125;&#125;</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">setValue</span> = (<span class="params">value</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> vMap = <span class="keyword">new</span> <span class="title class_">Map</span>()</span><br><span class="line">  value.<span class="title function_">forEach</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> [key, subVal] = val.<span class="title function_">split</span>(<span class="string">&#x27;.&#x27;</span>)</span><br><span class="line">    <span class="keyword">if</span> (!vMap.<span class="title function_">has</span>(key)) &#123;</span><br><span class="line">      vMap.<span class="title function_">set</span>(key, [])</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (subVal) &#123;</span><br><span class="line">      vMap.<span class="title function_">get</span>(key).<span class="title function_">push</span>(subVal)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">if</span> (!vMap.<span class="property">size</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">undefined</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">fromEntries</span>([...vMap.<span class="title function_">entries</span>()])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 下拉框渲染options的源数据</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@returns</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">localListToMap</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> localList = [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="string">&#x27;code&#x27;</span>: <span class="number">10</span>,</span><br><span class="line">      <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;分组1&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;children&#x27;</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="string">&#x27;code&#x27;</span>: <span class="number">1010</span>,</span><br><span class="line">          <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型一级&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;sub_type&#x27;</span>: [</span><br><span class="line">            &#123;</span><br><span class="line">              <span class="string">&#x27;code&#x27;</span>: <span class="number">1020</span>,</span><br><span class="line">              <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型二级1&#x27;</span></span><br><span class="line">            &#125;,</span><br><span class="line">          ]</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="string">&#x27;code&#x27;</span>: <span class="number">1011</span>,</span><br><span class="line">          <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型一级2&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;sub_type&#x27;</span>: []</span><br><span class="line">        &#125;,</span><br><span class="line">      ]</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="string">&#x27;code&#x27;</span>: <span class="number">20</span>,</span><br><span class="line">      <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;分组2&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;children&#x27;</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="string">&#x27;code&#x27;</span>: <span class="number">2010</span>,</span><br><span class="line">          <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型一级&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;sub_type&#x27;</span>: [</span><br><span class="line">            &#123;</span><br><span class="line">              <span class="string">&#x27;code&#x27;</span>: <span class="number">1020</span>,</span><br><span class="line">              <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型二级1&#x27;</span></span><br><span class="line">            &#125;,</span><br><span class="line">          ]</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">          <span class="string">&#x27;code&#x27;</span>: <span class="number">2011</span>,</span><br><span class="line">          <span class="string">&#x27;desc&#x27;</span>: <span class="string">&#x27;类型一级2&#x27;</span>,</span><br><span class="line">          <span class="string">&#x27;sub_type&#x27;</span>: []</span><br><span class="line">        &#125;,</span><br><span class="line">      ]</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">  <span class="keyword">const</span> localListMap = <span class="keyword">new</span> <span class="title class_">Map</span>()</span><br><span class="line">  <span class="comment">// 把最外层的分组跟第一级类型摊平</span></span><br><span class="line">  localList.<span class="title function_">forEach</span>(<span class="function">(<span class="params">parent</span>) =&gt;</span> &#123;</span><br><span class="line">    localListMap.<span class="title function_">set</span>(<span class="string">`<span class="subst">$&#123;parent.code&#125;</span>`</span>, &#123; <span class="attr">code</span>: <span class="string">`<span class="subst">$&#123;parent.code&#125;</span>`</span>, <span class="attr">desc</span>: parent.<span class="property">desc</span>, <span class="attr">isTitle</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">    parent.<span class="property">children</span>.<span class="title function_">forEach</span>(<span class="function"><span class="params">d</span> =&gt;</span> &#123;</span><br><span class="line">      localListMap.<span class="title function_">set</span>(<span class="string">`<span class="subst">$&#123;d.code&#125;</span>`</span>, &#123; ...d, <span class="attr">code</span>: <span class="string">`<span class="subst">$&#123;d.code&#125;</span>`</span>, <span class="attr">sub_type</span>: d.<span class="property">sub_type</span>?.<span class="title function_">map</span>(<span class="function"><span class="params">child</span> =&gt;</span> (&#123; ...child, <span class="attr">code</span>: <span class="string">`<span class="subst">$&#123;d.code&#125;</span>.<span class="subst">$&#123;child.code&#125;</span>`</span> &#125;)) &#125;)</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="comment">// 根据一级code和分组code 组成Map</span></span><br><span class="line">  <span class="keyword">return</span> localListMap</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">useSelectData</span> = (<span class="params">props, emit</span>) =&gt; &#123;</span><br><span class="line">  <span class="comment">// map结构，一级code做key</span></span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">RELATETYPELISTMAP</span> = <span class="title function_">localListToMap</span>()</span><br><span class="line">  <span class="comment">// 所有一级对应的数据，包含二级数据</span></span><br><span class="line">  <span class="keyword">const</span> <span class="variable constant_">RELATETYPELIST</span> = [...<span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">values</span>()]</span><br><span class="line">  <span class="comment">// 所有一级分组的选中态， filter过滤掉最外层只展示的分组</span></span><br><span class="line">  <span class="keyword">const</span> groupStatus = <span class="title function_">reactive</span>(</span><br><span class="line">    <span class="title class_">Object</span>.<span class="title function_">fromEntries</span>([...<span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">entries</span>()]</span><br><span class="line">      .<span class="title function_">filter</span>(<span class="function">(<span class="params">[key, val]</span>) =&gt;</span> !val.<span class="property">isTitle</span>)</span><br><span class="line">      .<span class="title function_">map</span>(<span class="function">(<span class="params">[key, val]</span>) =&gt;</span> [key, <span class="literal">false</span>]))</span><br><span class="line">  )</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 双向绑定的处理</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">const</span> selectValue = <span class="title function_">computed</span>(&#123;</span><br><span class="line">    <span class="attr">get</span>: <span class="function">() =&gt;</span> <span class="title function_">getValue</span>(props.<span class="property">value</span>),</span><br><span class="line">    <span class="attr">set</span>: <span class="function">(<span class="params">value</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> v = <span class="title function_">setValue</span>(value)</span><br><span class="line">      <span class="title function_">emit</span>(<span class="string">&#x27;update:value&#x27;</span>, v)</span><br><span class="line">      <span class="title function_">emit</span>(<span class="string">&#x27;changeRelateType&#x27;</span>, v)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 选项改变时</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleChange</span> = (<span class="params">value</span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 如果清空选项把group的选中态清空</span></span><br><span class="line">    <span class="keyword">if</span> (!value.<span class="property">length</span>) &#123;</span><br><span class="line">      <span class="title class_">Object</span>.<span class="title function_">keys</span>(groupStatus).<span class="title function_">forEach</span>(<span class="function"><span class="params">key</span> =&gt;</span> &#123;</span><br><span class="line">        groupStatus[key] = <span class="literal">false</span></span><br><span class="line">      &#125;)</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">const</span> selectedMap = <span class="keyword">new</span> <span class="title class_">Map</span>();</span><br><span class="line">    <span class="comment">// 将已选项处理成一个Map</span></span><br><span class="line">    value.<span class="title function_">forEach</span>(<span class="function">(<span class="params">v</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> [key, val] = v.<span class="title function_">split</span>(<span class="string">&#x27;.&#x27;</span>)</span><br><span class="line">      <span class="keyword">if</span> (!selectedMap.<span class="title function_">has</span>(key)) &#123;</span><br><span class="line">        selectedMap.<span class="title function_">set</span>(key, [])</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (val) &#123;</span><br><span class="line">        selectedMap.<span class="title function_">get</span>(key).<span class="title function_">push</span>(val)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="comment">// 获取已选项的一级的code</span></span><br><span class="line">    <span class="keyword">const</span> selectKeys = [...selectedMap.<span class="title function_">keys</span>()]</span><br><span class="line">    selectKeys.<span class="title function_">forEach</span>(<span class="function">(<span class="params">key</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// 比较已选的二级和源数据二级的长度</span></span><br><span class="line">      <span class="comment">// 长度相等则改变group的选中态</span></span><br><span class="line">      <span class="keyword">if</span> (selectedMap.<span class="title function_">get</span>(key).<span class="property">length</span> === (<span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">get</span>(key)?.<span class="property">sub_type</span> ?? []).<span class="property">length</span>) &#123;</span><br><span class="line">        groupStatus[key] = <span class="literal">true</span></span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        groupStatus[key] = <span class="literal">false</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 点击select group时</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> &#123;<span class="type">*</span>&#125; <span class="variable">group</span></span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleClickGroup</span> = (<span class="params">group</span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// title只做展示用的点了也不给他处理</span></span><br><span class="line">    <span class="keyword">if</span> (group.<span class="property">isTitle</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 获取子项</span></span><br><span class="line">    <span class="keyword">const</span> childrens = !<span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">get</span>(group.<span class="property">code</span>).<span class="property">sub_type</span>?.<span class="property">length</span> ? [group] : <span class="variable constant_">RELATETYPELISTMAP</span>.<span class="title function_">get</span>(group.<span class="property">code</span>).<span class="property">sub_type</span></span><br><span class="line">    <span class="comment">// 获取子项code</span></span><br><span class="line">    <span class="keyword">const</span> childrensCode = childrens.<span class="title function_">map</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val.<span class="property">code</span>)</span><br><span class="line">    <span class="comment">// 获取已选的code</span></span><br><span class="line">    <span class="keyword">const</span> selected = <span class="keyword">new</span> <span class="title class_">Set</span>(selectValue.<span class="property">value</span>)</span><br><span class="line">    <span class="comment">// 如果当前组已经是全选了</span></span><br><span class="line">    <span class="keyword">if</span> (groupStatus[group.<span class="property">code</span>]) &#123;</span><br><span class="line">      <span class="comment">// 删除已选的里面包含的子项</span></span><br><span class="line">      childrensCode.<span class="title function_">forEach</span>(<span class="function"><span class="params">code</span> =&gt;</span> &#123;</span><br><span class="line">        selected.<span class="title function_">delete</span>(code)</span><br><span class="line">      &#125;)</span><br><span class="line">      <span class="comment">// 重新给select赋值</span></span><br><span class="line">      selectValue.<span class="property">value</span> = [...selected]</span><br><span class="line">      <span class="comment">// 改变当前组的选中态</span></span><br><span class="line">      groupStatus[group.<span class="property">code</span>] = <span class="literal">false</span></span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 往里塞当前组的code</span></span><br><span class="line">    childrensCode.<span class="title function_">forEach</span>(<span class="function"><span class="params">code</span> =&gt;</span> &#123;</span><br><span class="line">      selected.<span class="title function_">add</span>(code)</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="comment">// 重新给select赋值</span></span><br><span class="line">    selectValue.<span class="property">value</span> = [...selected]</span><br><span class="line">    <span class="comment">// 改变当前组的选中态</span></span><br><span class="line">    groupStatus[group.<span class="property">code</span>] = <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="variable constant_">RELATETYPELIST</span>,</span><br><span class="line">    groupStatus,</span><br><span class="line">    selectValue,</span><br><span class="line">    handleChange,</span><br><span class="line">    handleClickGroup</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> useSelectData</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">el-select</span> <span class="attr">v-model</span>=<span class="string">&quot;selectValue&quot;</span> <span class="attr">:placeholder</span>=<span class="string">&quot;props.placeholder || &#x27;请选择&#x27;&quot;</span> <span class="attr">multiple</span> <span class="attr">clearable</span> <span class="attr">:disabled</span>=<span class="string">&quot;props.disabled&quot;</span> @<span class="attr">change</span>=<span class="string">&quot;handleChange&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">template</span> <span class="attr">v-for</span>=<span class="string">&quot;group in RELATETYPELIST&quot;</span>&gt;</span></span><br><span class="line">      <span class="comment">&lt;!-- 分组 --&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">el-option</span> <span class="attr">class</span>=<span class="string">&quot;el-select-group__title group&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;group.code&quot;</span> <span class="attr">v-if</span>=<span class="string">&quot;group.isTitle&quot;</span> <span class="attr">:label</span>=<span class="string">&quot;`$&#123;group.desc&#125;`&quot;</span> <span class="attr">:value</span>=<span class="string">&quot;group.code&quot;</span> <span class="attr">disabled</span>&gt;</span>&#123;&#123; group.desc &#125;&#125;<span class="tag">&lt;/<span class="name">el-option</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">template</span> <span class="attr">v-else</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 两级，这个充当一级 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span></span></span><br><span class="line"><span class="tag">          <span class="attr">v-if</span>=<span class="string">&quot;group.sub_type &amp;&amp; !!group.sub_type.length&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:key</span>=<span class="string">&quot;group.code&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">class</span>=<span class="string">&quot;el-select-group__title group can-select&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:class</span>=<span class="string">&quot;&#123; &#x27;all-selected&#x27;: groupStatus[group.code] &#125;&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:label</span>=<span class="string">&quot;`$&#123;group.desc&#125;`&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:value</span>=<span class="string">&quot;group.code&quot;</span></span></span><br><span class="line"><span class="tag">          @<span class="attr">click</span>=<span class="string">&quot;handleClickGroup(group)&quot;</span></span></span><br><span class="line"><span class="tag">        &gt;</span>&#123;&#123; group.desc &#125;&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 只有一级的时候 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">el-option</span> <span class="attr">v-else</span> <span class="attr">class</span>=<span class="string">&quot;group can-select&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;group.code&quot;</span> <span class="attr">:label</span>=<span class="string">&quot;`$&#123;group.desc&#125;`&quot;</span> <span class="attr">:value</span>=<span class="string">&quot;group.code&quot;</span>&gt;</span>&#123;&#123; group.desc &#125;&#125;<span class="tag">&lt;/<span class="name">el-option</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">el-option</span> <span class="attr">class</span>=<span class="string">&quot;pl-30&quot;</span> <span class="attr">v-for</span>=<span class="string">&quot;item in group.sub_type&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;item.code&quot;</span> <span class="attr">:label</span>=<span class="string">&quot;`$&#123;group.desc&#125;-$&#123;item.desc&#125;`&quot;</span> <span class="attr">:value</span>=<span class="string">&quot;item.code&quot;</span>&gt;</span></span><br><span class="line">          &#123;&#123;item.desc&#125;&#125;</span><br><span class="line">        <span class="tag">&lt;/<span class="name">el-option</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">el-select</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> useSelectData, &#123; propsType, emitType &#125; <span class="keyword">from</span> <span class="string">&#x27;./useSelectData&#x27;</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; onMounted &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> props = <span class="title function_">defineProps</span>(propsType)</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> emit = <span class="title function_">defineEmits</span>(emitType)</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> &#123; selectValue, groupStatus, <span class="variable constant_">RELATETYPELIST</span>, handleChange, handleClickGroup &#125; = <span class="title function_">useSelectData</span>(props, emit)</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="title function_">onMounted</span>(<span class="function">() =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="title function_">handleChange</span>(selectValue.<span class="property">value</span>)</span></span><br><span class="line"><span class="language-javascript">&#125;)</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span> <span class="attr">scoped</span> <span class="attr">lang</span>=<span class="string">&quot;scss&quot;</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css"><span class="selector-class">.group</span> &#123;</span></span><br><span class="line"><span class="language-css">  &amp;<span class="selector-class">.can-select</span> &#123;</span></span><br><span class="line"><span class="language-css">    <span class="attribute">cursor</span>: pointer;</span></span><br><span class="line"><span class="language-css">  &#125;</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"><span class="selector-class">.all-selected</span> &#123;</span></span><br><span class="line"><span class="language-css">  <span class="attribute">color</span>: <span class="number">#409EFF</span>;</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"><span class="selector-class">.pl-30</span> &#123;</span></span><br><span class="line"><span class="language-css">  <span class="attribute">padding-left</span>: <span class="number">30px</span>;</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.wybyte.top/2023/07/18/%E5%9F%BA%E4%BA%8Eelement-select%E5%A4%9A%E5%88%86%E7%BB%84%E5%A4%9A%E7%BA%A7%E4%B8%8B%E6%8B%89%E7%AD%9B%E9%80%89%E5%B0%81%E8%A3%85/</id>
    <link href="https://blog.wybyte.top/2023/07/18/%E5%9F%BA%E4%BA%8Eelement-select%E5%A4%9A%E5%88%86%E7%BB%84%E5%A4%9A%E7%BA%A7%E4%B8%8B%E6%8B%89%E7%AD%9B%E9%80%89%E5%B0%81%E8%A3%85/"/>
    <published>2023-07-18T11:51:03.000Z</published>
    <summary>
      <![CDATA[<h2 id="基于element-select多分组多级下拉筛选封装"><a href="#基于element-select多分组多级下拉筛选封装" class="headerlink" title="基于element select多分组多级下拉筛选封装"></a>基于ele]]>
    </summary>
    <title>基于element select多分组多级下拉筛选封装</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="Vue" scheme="https://blog.wybyte.top/categories/Vue/"/>
    <category term="CreateApp" scheme="https://blog.wybyte.top/categories/Vue/CreateApp/"/>
    <category term="Vue" scheme="https://blog.wybyte.top/tags/Vue/"/>
    <category term="源码分析" scheme="https://blog.wybyte.top/tags/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
    <content>
      <![CDATA[<h2 id="Vue源码分析-CreateApp"><a href="#Vue源码分析-CreateApp" class="headerlink" title="Vue源码分析-CreateApp"></a>Vue源码分析-CreateApp</h2><h3 id="官网解释"><a href="#官网解释" class="headerlink" title="官网解释"></a>官网解释</h3><p>创建一个应用实例</p><ul><li><p>类型</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">createApp</span>(<span class="params"><span class="attr">rootComponent</span>: <span class="title class_">Component</span>, <span class="attr">rootProps</span>?: <span class="built_in">object</span></span>): <span class="title class_">App</span></span><br></pre></td></tr></table></figure></li><li><p>详细信息</p><p>第一个参数是根组件。第二个参数可选，它是要传递给根组件的 props。</p></li><li><p>示例</p><p>可以直接内联根组件：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createApp &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> app = <span class="title function_">createApp</span>(&#123;</span><br><span class="line">  <span class="comment">/* root component options */</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>也可以使用从别处导入的组件：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createApp &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">App</span> <span class="keyword">from</span> <span class="string">&#x27;./App.vue&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> app = <span class="title function_">createApp</span>(<span class="title class_">App</span>)</span><br></pre></td></tr></table></figure></li><li><p>源码位置</p><p>packages&#x2F;runtime-core&#x2F;src&#x2F;apiCreateApp.ts</p></li><li><p>函数名</p><p>createAppAPI</p></li><li><p>mini-vue中的实现（简化版vue实现）</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">createAppAPI</span>(<span class="params">render</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">function</span> <span class="title function_">createApp</span>(<span class="params">rootComponent</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> app = &#123;</span><br><span class="line">      <span class="attr">_component</span>: rootComponent,</span><br><span class="line">      <span class="title function_">mount</span>(<span class="params">rootContainer</span>) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;基于根组件创建 vnode&quot;</span>);</span><br><span class="line">        <span class="keyword">const</span> vnode = <span class="title function_">createVNode</span>(rootComponent);</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;调用 render，基于 vnode 进行开箱&quot;</span>);</span><br><span class="line">        <span class="title function_">render</span>(vnode, rootContainer);</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> app;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><h3 id="createApp源码内函数处理"><a href="#createApp源码内函数处理" class="headerlink" title="createApp源码内函数处理"></a>createApp源码内函数处理</h3><ul><li><p>部分源码</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> createAppAPI&lt;<span class="title class_">HostElement</span>&gt;(</span><br><span class="line">  <span class="attr">render</span>: <span class="title class_">RootRenderFunction</span>&lt;<span class="title class_">HostElement</span>&gt;,</span><br><span class="line">  <span class="attr">hydrate</span>?: <span class="title class_">RootHydrateFunction</span></span><br><span class="line">): <span class="title class_">CreateAppFunction</span>&lt;<span class="title class_">HostElement</span>&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (!<span class="title function_">isFunction</span>(rootComponent)) &#123;</span><br><span class="line">    rootComponent = <span class="title function_">extend</span>(&#123;&#125;, rootComponent)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (rootProps != <span class="literal">null</span> &amp;&amp; !<span class="title function_">isObject</span>(rootProps)) &#123;</span><br><span class="line">    __DEV__ &amp;&amp; <span class="title function_">warn</span>(<span class="string">`root props passed to app.mount() must be an object.`</span>)</span><br><span class="line">    rootProps = <span class="literal">null</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> context = <span class="title function_">createAppContext</span>()</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="attr">app</span>: <span class="title class_">App</span> = (context.<span class="property">app</span> = &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (__COMPAT__) &#123;</span><br><span class="line">    <span class="title function_">installAppCompatProperties</span>(app, context, render)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> app</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>创建context源码</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// createApp经过一些列判断之后使用createAppContext，创建了一个空的根组件对象</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">createAppContext</span>(<span class="params"></span>): <span class="title class_">AppContext</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">app</span>: <span class="literal">null</span> <span class="keyword">as</span> <span class="built_in">any</span>,</span><br><span class="line">    <span class="attr">config</span>: &#123;</span><br><span class="line">      <span class="attr">isNativeTag</span>: <span class="variable constant_">NO</span>,</span><br><span class="line">      <span class="attr">performance</span>: <span class="literal">false</span>,</span><br><span class="line">      <span class="attr">globalProperties</span>: &#123;&#125;,</span><br><span class="line">      <span class="attr">optionMergeStrategies</span>: &#123;&#125;,</span><br><span class="line">      <span class="attr">errorHandler</span>: <span class="literal">undefined</span>,</span><br><span class="line">      <span class="attr">warnHandler</span>: <span class="literal">undefined</span>,</span><br><span class="line">      <span class="attr">compilerOptions</span>: &#123;&#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">mixins</span>: [],</span><br><span class="line">    <span class="attr">components</span>: &#123;&#125;,</span><br><span class="line">    <span class="attr">directives</span>: &#123;&#125;,</span><br><span class="line">    <span class="attr">provides</span>: <span class="title class_">Object</span>.<span class="title function_">create</span>(<span class="literal">null</span>),</span><br><span class="line">    <span class="attr">optionsCache</span>: <span class="keyword">new</span> <span class="title class_">WeakMap</span>(),</span><br><span class="line">    <span class="attr">propsCache</span>: <span class="keyword">new</span> <span class="title class_">WeakMap</span>(),</span><br><span class="line">    <span class="attr">emitsCache</span>: <span class="keyword">new</span> <span class="title class_">WeakMap</span>()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>创建完context后，给context.app赋值, 其中包含的属性及含义：</p><ul><li><p>_uid</p></li><li><p>_component（createApp传入的rootComponent处理来的）</p></li><li><p>_props（createApp传入的rootProps处理来的）</p></li><li><p>_container mount传入的rootContainer</p></li><li><p>_context（context实例）</p></li><li><p>_instance (mount时创建的vnode的component)</p></li><li><p>version（Vue版本）</p></li><li><p>config</p><p>特殊处理设置getter、setter</p><p>getter 返回 context.config</p><p>setter 开发环境提示不让直接更改该属性</p></li><li><p>use 对应app.use(plugin: Plugin, …options: any[])</p></li><li><p>mixin 方法</p><p>如果支持选项式API则将传入的mixin存放到context.mixins<br>内部有去重处理</p></li><li><p>component 同app.component()</p></li><li><p>directive 同 app.directive()</p></li><li><p>mount</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">mount</span>(</span><br><span class="line">  <span class="attr">rootContainer</span>: <span class="title class_">HostElement</span>,</span><br><span class="line">  <span class="attr">isHydrate</span>?: <span class="built_in">boolean</span>,</span><br><span class="line">  <span class="attr">isSVG</span>?: <span class="built_in">boolean</span></span><br><span class="line">): <span class="built_in">any</span> &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="comment">// 创建vnode</span></span><br><span class="line">  <span class="keyword">const</span> vnode = <span class="title function_">createVNode</span>(rootComponent, rootProps)</span><br><span class="line">  <span class="comment">// store app context on the root VNode.</span></span><br><span class="line">  <span class="comment">// this will be set on the root instance on initial mount.</span></span><br><span class="line">  <span class="comment">// 将当前的上下文给vnode挂上</span></span><br><span class="line">  vnode.<span class="property">appContext</span> = context</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="comment">// 判断用那个函数渲染</span></span><br><span class="line">  <span class="keyword">if</span> (isHydrate &amp;&amp; hydrate) &#123;</span><br><span class="line">    <span class="title function_">hydrate</span>(vnode <span class="keyword">as</span> <span class="title class_">VNode</span>&lt;<span class="title class_">Node</span>, <span class="title class_">Element</span>&gt;, rootContainer <span class="keyword">as</span> <span class="built_in">any</span>)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="title function_">render</span>(vnode, rootContainer, isSVG)</span><br><span class="line">  &#125;</span><br><span class="line">  app.<span class="property">_container</span> = rootContainer <span class="comment">// 将mount传入的rootContainer赋值给_container</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (__DEV__ || __FEATURE_PROD_DEVTOOLS__) &#123;</span><br><span class="line">    <span class="comment">// 将vnode的component赋值给app的_instance</span></span><br><span class="line">    app.<span class="property">_instance</span> = vnode.<span class="property">component</span></span><br><span class="line">    <span class="title function_">devtoolsInitApp</span>(app, version)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 返回一个根组件实例</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">getExposeProxy</span>(vnode.<span class="property">component</span>!) || vnode.<span class="property">component</span>!.<span class="property">proxy</span></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>unmount</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">unmount</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="title function_">render</span>(<span class="literal">null</span>, app.<span class="property">_container</span>) <span class="comment">// 卸载应用</span></span><br><span class="line">  <span class="comment">// ... </span></span><br><span class="line">  app.<span class="property">_instance</span> = <span class="literal">null</span> <span class="comment">// 将_instance赋空</span></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">delete</span> app.<span class="property">_container</span>.<span class="property">__vue_app__</span> <span class="comment">// 删除__vue_app__标记</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>provide</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">provide</span>(<span class="params">key, value</span>) &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="comment">// 提供一个值，可以在应用中的所有后代组件中注入使用</span></span><br><span class="line">  context.<span class="property">provides</span>[key <span class="keyword">as</span> <span class="built_in">string</span> | <span class="built_in">symbol</span>] = value</span><br><span class="line">  <span class="comment">// 返回当前实例可以链式调用</span></span><br><span class="line">  <span class="keyword">return</span> app</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>runWithContext <code>3.3+</code></p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">runWithContext</span>(<span class="params">fn</span>) &#123;</span><br><span class="line">  <span class="comment">// 当前应用</span></span><br><span class="line">  currentApp = app</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="comment">// 执行回调函数</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">fn</span>()</span><br><span class="line">  &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    currentApp = <span class="literal">null</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><blockquote><p>app属性参考，官网地址: <a href="https://cn.vuejs.org/api/application.html">https://cn.vuejs.org/api/application.html</a></p></blockquote></li></ul>]]>
    </content>
    <id>https://blog.wybyte.top/2023/05/23/Vue-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-1/</id>
    <link href="https://blog.wybyte.top/2023/05/23/Vue-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-1/"/>
    <published>2023-05-23T15:07:26.000Z</published>
    <summary>
      <![CDATA[<h2 id="Vue源码分析-CreateApp"><a href="#Vue源码分析-CreateApp" class="headerlink" title="Vue源码分析-CreateApp"></a>Vue源码分析-CreateApp</h2><h3 id="官网解释"]]>
    </summary>
    <title>Vue 源码解析(1)</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="打包工具" scheme="https://blog.wybyte.top/categories/%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7/"/>
    <category term="vite" scheme="https://blog.wybyte.top/categories/%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7/vite/"/>
    <category term="打包工具" scheme="https://blog.wybyte.top/tags/%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7/"/>
    <category term="vite" scheme="https://blog.wybyte.top/tags/vite/"/>
    <content>
      <![CDATA[<h2 id="为什么选择vite"><a href="#为什么选择vite" class="headerlink" title="为什么选择vite"></a>为什么选择vite</h2><p>借用vite官网的介绍<a href="%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9vite">https://cn.vitejs.dev/guide/why.html#the-problems</a><br>本人更换vite主要就是两个原因：</p><ol><li>webpack dev环境启动太慢了，公司项目启动平均要一分半钟；</li><li>webpack build需要的内存较大，公司项目平均2.3G内存；</li></ol><h2 id="初步准备"><a href="#初步准备" class="headerlink" title="初步准备"></a>初步准备</h2><ul><li>如果不想要webpack和vite共存，那么我建议直接新建一个文件夹。</li><li>我所用的项目为vue2.7项目（所以我会讲我用到的vue2.x的包，3.x的建议直接yarn create vite my-vue-app –template vue）。</li></ul><h2 id="更换过程"><a href="#更换过程" class="headerlink" title="更换过程"></a>更换过程</h2><ul><li><p>首先 npm init 创建package.json， 已有项目直接复制过来就好</p></li><li><p>安装必要的npm包，<code>vite</code>, <code>@vitejs/plugin-vue2</code>(vue3.x安装<code>@vitejs/plugin-vue</code>)，项目中需要的其他包根据自己的需要添加，以上为vite运行必须的包</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i vite @vitejs/plugin-vue2 -D</span><br></pre></td></tr></table></figure></li><li><p>新建vite.config.js文件</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vite.config.js</span></span><br><span class="line"><span class="keyword">import</span> &#123; defineConfig &#125; <span class="keyword">from</span> <span class="string">&#x27;vite&#x27;</span></span><br><span class="line"><span class="keyword">import</span> vue <span class="keyword">from</span> <span class="string">&#x27;@vitejs/plugin-vue2&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; fileURLToPath &#125; <span class="keyword">from</span> <span class="string">&#x27;url&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(<span class="function">(<span class="params">&#123; command, mode &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  reutrn &#123;</span><br><span class="line">    <span class="comment">// 写vue的一般都用‘@’作为‘src’的别名，我这里就配了这一个，有需要则自行添加</span></span><br><span class="line">    <span class="attr">resolve</span>: &#123;</span><br><span class="line">      <span class="attr">alias</span>: &#123;</span><br><span class="line">        <span class="string">&#x27;@&#x27;</span>: <span class="title function_">fileURLToPath</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">&#x27;./src&#x27;</span>, <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>))</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="attr">extensions</span>: [<span class="string">&#x27;.js&#x27;</span>, <span class="string">&#x27;.vue&#x27;</span>]</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">plugins</span>: [</span><br><span class="line">      <span class="comment">// 解析vue2.x需要的插件</span></span><br><span class="line">      <span class="title function_">vue</span>()</span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">server</span>: &#123;</span><br><span class="line">      <span class="comment">// dev环境代理服务</span></span><br><span class="line">      <span class="attr">proxy</span>: &#123;</span><br><span class="line">        <span class="string">&#x27;/api&#x27;</span>: &#123;</span><br><span class="line">          <span class="attr">target</span>: <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">          <span class="attr">changeOrigin</span>: <span class="literal">true</span>,</span><br><span class="line">          <span class="attr">rewrite</span>: <span class="function">(<span class="params">path</span>) =&gt;</span> path.<span class="title function_">replace</span>(<span class="string">&#x27;/api&#x27;</span>, <span class="string">&#x27;&#x27;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 打包时去掉debugger和console</span></span><br><span class="line">    <span class="attr">esbuild</span>: &#123;</span><br><span class="line">      <span class="attr">drop</span>: command === <span class="string">&#x27;build&#x27;</span> ? [<span class="string">&#x27;debugger&#x27;</span>, <span class="string">&#x27;console&#x27;</span>] : [],</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure></li><li><p>此时将旧版项目的src复制到新的项目下，目录结构为</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">src/</span><br><span class="line">  compontents/</span><br><span class="line">    **.vue</span><br><span class="line">  router/</span><br><span class="line">    index.js</span><br><span class="line">  store/</span><br><span class="line">    index.js</span><br><span class="line">  utils/</span><br><span class="line">  App.vue</span><br><span class="line">  main.js</span><br><span class="line">index.html</span><br><span class="line">jsconfig.json</span><br><span class="line">package.json</span><br><span class="line">vite.config.js</span><br></pre></td></tr></table></figure></li><li><p>main.js 需要进行调整</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// main.js</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 只有new Vue部分修改，其余部分可继续沿用，例如element-ui</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Vue</span> <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> router <span class="keyword">from</span> <span class="string">&#x27;./router&#x27;</span></span><br><span class="line"><span class="keyword">import</span> store <span class="keyword">from</span> <span class="string">&#x27;./store/index.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">ElementUI</span> <span class="keyword">from</span> <span class="string">&#x27;element-ui&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;element-ui/lib/theme-chalk/index.css&#x27;</span>;</span><br><span class="line"><span class="title class_">Vue</span>.<span class="title function_">use</span>(<span class="title class_">ElementUI</span>); <span class="comment">// 全局使用ElementUI</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">App</span> <span class="keyword">from</span> <span class="string">&#x27;./App&#x27;</span></span><br><span class="line"><span class="comment">// 原</span></span><br><span class="line"><span class="comment">// const instance = new Vue(&#123;</span></span><br><span class="line"><span class="comment">//   el: &#x27;#app&#x27;,</span></span><br><span class="line"><span class="comment">//   router,</span></span><br><span class="line"><span class="comment">//   store,</span></span><br><span class="line"><span class="comment">//   components: &#123; App &#125;,</span></span><br><span class="line"><span class="comment">//   template: &#x27;&lt;App/&gt;&#x27;</span></span><br><span class="line"><span class="comment">// &#125;)</span></span><br><span class="line"><span class="comment">// export default instance</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 现</span></span><br><span class="line"><span class="keyword">const</span> instance = <span class="keyword">new</span> <span class="title class_">Vue</span>(&#123;</span><br><span class="line">  router,</span><br><span class="line">  store,</span><br><span class="line">  <span class="attr">render</span>: <span class="function">(<span class="params">h</span>) =&gt;</span> <span class="title function_">h</span>(<span class="title class_">App</span>)</span><br><span class="line">&#125;).$mount(<span class="string">&#x27;#app&#x27;</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> instance</span><br></pre></td></tr></table></figure></li><li><p>index.html需要增加main.js的引用</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;app&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;module&quot;</span> <span class="attr">src</span>=<span class="string">&quot;/src/main.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br></pre></td></tr></table></figure></li></ul><p>此时项目应该可以运行起来了</p><h2 id="npm包在生产环境进行cdn引入"><a href="#npm包在生产环境进行cdn引入" class="headerlink" title="npm包在生产环境进行cdn引入"></a>npm包在生产环境进行cdn引入</h2><p>本人使用<code>rollup-plugin-external-globals</code>包进行cdn的使用</p><ul><li><p>安装 <code>rollup-plugin-external-globals</code>包</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i rollup-plugin-external-globals -D</span><br></pre></td></tr></table></figure></li><li><p>修改vite.config.js</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vite.config.js</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 省略上面配置过的</span></span><br><span class="line">... </span><br><span class="line"><span class="keyword">import</span> externalGlobals <span class="keyword">from</span> <span class="string">&#x27;rollup-plugin-external-globals&#x27;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(<span class="function">(<span class="params">&#123; command, mode &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  reutrn &#123;</span><br><span class="line">    <span class="comment">// 省略上面配置过的 </span></span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 增加build打包后cdn的配置</span></span><br><span class="line">    <span class="attr">build</span>: &#123;</span><br><span class="line">      <span class="attr">rollupOptions</span>: &#123;</span><br><span class="line">        <span class="comment">// 代码中引用的包名</span></span><br><span class="line">        <span class="attr">external</span>: [<span class="string">&#x27;vue&#x27;</span>, <span class="string">&#x27;vue-router&#x27;</span>, <span class="string">&#x27;element-ui&#x27;</span>],</span><br><span class="line">        <span class="attr">plugins</span>: [</span><br><span class="line">          <span class="comment">// 属性名为项目中使用的名字，值为cdn暴露的全局变量名</span></span><br><span class="line">          <span class="title function_">externalGlobals</span>(&#123;</span><br><span class="line">            <span class="attr">vue</span>: <span class="string">&#x27;Vue&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;vue-router&#x27;</span>: <span class="string">&#x27;VueRouter&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;element-ui&#x27;</span>: <span class="string">&#x27;ELEMENT&#x27;</span>,</span><br><span class="line">          &#125;)</span><br><span class="line">        ]</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure></li><li><p>修改index.html</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;stylesheet&quot;</span> <span class="attr">href</span>=<span class="string">&quot;https://unpkg.com/element-ui@2.15.10/lib/theme-chalk/index.css&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">link</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;app&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;https://unpkg.com/vue@2.7.13/dist/vue.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;https://unpkg.com/element-ui@2.15.10&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;https://unpkg.com/vue-router@3.6.5&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;module&quot;</span> <span class="attr">src</span>=<span class="string">&quot;/src/main.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br></pre></td></tr></table></figure></li></ul><p>此时打包已经将<code>vue</code>、<code>element-ui</code>、<code>vue-router</code>排除了。</p><div class="note info flat"><p>使用<code>vite-plugin-html</code>可以进行html内的cdn自动导入</p></div><h2 id="cdn自动引入"><a href="#cdn自动引入" class="headerlink" title="cdn自动引入"></a>cdn自动引入</h2><p>使用<code>vite-plugin-html</code>包完成cdn自动引入index.html</p><ul><li><p>安装</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i vite-plugin-html -D</span><br></pre></td></tr></table></figure></li><li><p>创建一个js文件，我这里取名cdn.js</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// config/cdn.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="comment">// 属性名自己起</span></span><br><span class="line">  <span class="attr">js</span>: [</span><br><span class="line">    <span class="string">&#x27;https://unpkg.com/vue@2.7.13/dist/vue.js&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;https://unpkg.com/element-ui@2.15.10&#x27;</span>,</span><br><span class="line">    <span class="string">&#x27;https://unpkg.com/vue-router@3.6.5&#x27;</span></span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">css</span>: [</span><br><span class="line">    <span class="string">&#x27;https://unpkg.com/element-ui@2.15.10/lib/theme-chalk/index.css&#x27;</span></span><br><span class="line">  ]</span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure></li><li><p>修改vite.config.js</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vite.config.js</span></span><br><span class="line"><span class="comment">// 省略上面配置过的</span></span><br><span class="line">... </span><br><span class="line"><span class="keyword">import</span> &#123; createHtmlPlugin &#125; <span class="keyword">from</span> <span class="string">&#x27;vite-plugin-html&#x27;</span></span><br><span class="line"><span class="keyword">import</span> cdn <span class="keyword">from</span> <span class="string">&#x27;/config/cdn.js&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(<span class="function">(<span class="params">&#123; command, mode &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  reutrn &#123;</span><br><span class="line">    <span class="comment">// 省略上面配置过的 </span></span><br><span class="line">    ...</span><br><span class="line">    <span class="attr">plugins</span>: [</span><br><span class="line">      ...</span><br><span class="line">      <span class="title function_">createHtmlPlugin</span>(&#123;</span><br><span class="line">        <span class="attr">minify</span>: <span class="literal">true</span>, <span class="comment">// 是否压缩 html</span></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * 如果你想将 `index.html`存放在指定文件夹，可以修改它，否则不需要配置</span></span><br><span class="line"><span class="comment">        * <span class="doctag">@default</span> index.html</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line">        <span class="attr">template</span>: <span class="string">&quot;/index.html&quot;</span>,</span><br><span class="line">        <span class="attr">inject</span>: &#123;</span><br><span class="line">          <span class="attr">data</span>: &#123;</span><br><span class="line">            <span class="comment">// 这个属性名也是自己起</span></span><br><span class="line">            <span class="attr">injectScript</span>: cdn.<span class="property">js</span>,</span><br><span class="line">            <span class="attr">injectLink</span>: cdn.<span class="property">css</span>,</span><br><span class="line">          &#125;,</span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;)</span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure></li><li><p>修改index.html</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">  &lt;% for (let i in injectLink) &#123; %&gt;</span><br><span class="line">    <span class="tag">&lt;<span class="name">link</span> <span class="attr">rel</span>=<span class="string">&quot;stylesheet&quot;</span> <span class="attr">href</span>=<span class="string">&quot;&lt;%= injectLink[i] %&gt;&quot;</span> /&gt;</span></span><br><span class="line">  &lt;% &#125; %&gt;</span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;app&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="comment">&lt;!-- 这里的injectScript就是vite.config.js里自己起的名 --&gt;</span></span><br><span class="line">  &lt;% for (let i in injectScript) &#123; %&gt;</span><br><span class="line">    <span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;&lt;%= injectScript[i] %&gt;&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">  &lt;% &#125; %&gt;</span><br><span class="line"></span><br><span class="line">  <span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;module&quot;</span> <span class="attr">src</span>=<span class="string">&quot;/src/main.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br></pre></td></tr></table></figure></li></ul><h2 id="启用Gzip"><a href="#启用Gzip" class="headerlink" title="启用Gzip"></a>启用Gzip</h2><ul><li><p>安装<code>vite-plugin-compression</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i vite-plugin-compression -D</span><br></pre></td></tr></table></figure></li><li><p>vite.config.js使用</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vite.config.js</span></span><br><span class="line">... <span class="comment">// 省略上面已有的</span></span><br><span class="line"><span class="keyword">import</span> viteCompression <span class="keyword">from</span> <span class="string">&#x27;vite-plugin-compression&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(<span class="function">(<span class="params">&#123; command, mode &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  reutrn &#123;</span><br><span class="line">    <span class="comment">// 省略上面配置过的 </span></span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 增加build打包后cdn的配置</span></span><br><span class="line">    <span class="attr">build</span>: &#123;</span><br><span class="line">      <span class="attr">rollupOptions</span>: &#123;</span><br><span class="line">        ...</span><br><span class="line">        <span class="attr">plugins</span>: [</span><br><span class="line">          ...</span><br><span class="line">          <span class="title function_">viteCompression</span>()</span><br><span class="line">        ]</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure></li></ul><h2 id="启用打包分析"><a href="#启用打包分析" class="headerlink" title="启用打包分析"></a>启用打包分析</h2><ul><li><p>安装<code>rollup-plugin-visualizer</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i rollup-plugin-visualizer -D</span><br></pre></td></tr></table></figure></li><li><p>vite.config.js中使用</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vite.config.js</span></span><br><span class="line"><span class="comment">// 省略上面配置过的</span></span><br><span class="line">... </span><br><span class="line"><span class="keyword">import</span> &#123; visualizer &#125; <span class="keyword">from</span> <span class="string">&#x27;rollup-plugin-visualizer&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(<span class="function">(<span class="params">&#123; command, mode &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  reutrn &#123;</span><br><span class="line">    <span class="comment">// 省略上面配置过的 </span></span><br><span class="line">    ...</span><br><span class="line">    <span class="attr">plugins</span>: [</span><br><span class="line">      ...</span><br><span class="line">      <span class="title function_">visualizer</span>(&#123; <span class="attr">open</span>: <span class="literal">true</span> &#125;)</span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure></li></ul><h2 id="插件总结"><a href="#插件总结" class="headerlink" title="插件总结"></a>插件总结</h2><ul><li><code>vite</code> vite包</li><li><code>@vitejs/plugin-vue2</code> vue2.x语法解析</li><li><code>vite-plugin-compression</code> Gzip压缩</li><li><code>rollup-plugin-visualizer</code> 打包依赖分析</li><li><code>vite-plugin-eslint</code> eslint校验,需配合<code>eslint</code>, <code>@babel/eslint-parser</code>, <code>eslint-plugin-vue</code></li><li><code>@vitejs/plugin-vue2-jsx</code> vue2.x jsx插件</li><li><code>vite-plugin-mock</code> mock插件，需配合<code>mockjs</code>包</li><li><code>vite-plugin-html</code> html模版自动导入cdn</li></ul>]]>
    </content>
    <id>https://blog.wybyte.top/2022/11/07/webpack%E6%9B%B4%E6%8D%A2vite/</id>
    <link href="https://blog.wybyte.top/2022/11/07/webpack%E6%9B%B4%E6%8D%A2vite/"/>
    <published>2022-11-07T14:47:01.000Z</published>
    <summary>
      <![CDATA[<h2 id="为什么选择vite"><a href="#为什么选择vite" class="headerlink" title="为什么选择vite"></a>为什么选择vite</h2><p>借用vite官网的介绍<a href="%E4%B8%BA%E4%BB%80%E4%]]>
    </summary>
    <title>webpack更换vite</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="算法" scheme="https://blog.wybyte.top/categories/%E7%AE%97%E6%B3%95/"/>
    <category term="队列" scheme="https://blog.wybyte.top/categories/%E7%AE%97%E6%B3%95/%E9%98%9F%E5%88%97/"/>
    <category term="JavaScript" scheme="https://blog.wybyte.top/tags/JavaScript/"/>
    <category term="算法" scheme="https://blog.wybyte.top/tags/%E7%AE%97%E6%B3%95/"/>
    <content>
      <![CDATA[<h1 id="队列的概念"><a href="#队列的概念" class="headerlink" title="队列的概念"></a>队列的概念</h1><p>​队列是遵循FIFO（First In First Out，先进先出，也称为先来先服务）原则的一组有序的项。意如其名，就像排队一样，从尾部添加，从顶部移除，新添的元素必须排在队列的末尾。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">flowchart LR</span><br><span class="line">subgraph a[队列]</span><br><span class="line">q[...]--&gt;3--&gt;2</span><br><span class="line">end</span><br><span class="line"></span><br><span class="line">subgraph b[执行]</span><br><span class="line">q1[1]</span><br><span class="line">end</span><br><span class="line">2--&gt;q1</span><br></pre></td></tr></table></figure><h1 id="创建队列"><a href="#创建队列" class="headerlink" title="创建队列"></a>创建队列</h1><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Queue</span> &#123;</span><br><span class="line">  <span class="attr">items</span>: <span class="title class_">Array</span>&lt;<span class="built_in">any</span>&gt;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">items</span> = []</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 入队</span></span><br><span class="line"><span class="comment">   * 向队列尾部添加一个（或多个）新的项</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> <span class="variable">el</span></span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">enqueue</span>(el): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (el <span class="keyword">instanceof</span> <span class="title class_">Array</span>) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">items</span>.<span class="title function_">push</span>(...el)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">items</span>.<span class="title function_">push</span>(el)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 出队</span></span><br><span class="line"><span class="comment">   * 移除队列的第一（即排在队列最前面的）项，并返回被移除的元素</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">dequeue</span>(): <span class="built_in">any</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>.<span class="title function_">shift</span>()</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 返回队列中第一个元素——最先被添加，也将是最先被移除的元素。</span></span><br><span class="line"><span class="comment">   * 队列不做任何变动（不移除元素，只返回元素信息——与Stack类的peek方法非常类似）</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">front</span>(): <span class="built_in">any</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>[<span class="number">0</span>]</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 判断队列是否为空</span></span><br><span class="line"><span class="comment">   * 如果队列中不包含任何元素，返回true，否则返回false</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">isEmpty</span>(): <span class="built_in">boolean</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>.<span class="property">length</span> === <span class="number">0</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 返回队列包含的元素个数</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">size</span>(): <span class="built_in">number</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>.<span class="property">length</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="优先队列"><a href="#优先队列" class="headerlink" title="优先队列"></a>优先队列</h1><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">QueueElement</span>&lt;T, K&gt; &#123;</span><br><span class="line">  <span class="attr">element</span>: T</span><br><span class="line">  <span class="attr">priority</span>: K</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">el, priority</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">element</span> = el</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">priority</span> = priority</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PriorityQueue</span> &#123;</span><br><span class="line">  <span class="attr">items</span>: <span class="title class_">Array</span>&lt;<span class="built_in">any</span>&gt;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">items</span> = []</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@desc</span> 入队</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> el &#123;T&#125; 需要入队的元素</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> priority &#123;number&#125; 优先级</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  enqueue&lt;T&gt; (<span class="attr">el</span>: T, <span class="attr">priority</span>: <span class="built_in">number</span>): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="comment">// 创建一个带优先级的对象</span></span><br><span class="line">    <span class="keyword">const</span> queueElement = <span class="keyword">new</span> <span class="title class_">QueueElement</span>&lt;T, <span class="built_in">number</span>&gt;(el, priority)</span><br><span class="line">    <span class="comment">// 判断队列是否为空，空就直接加进去</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="title function_">isEmpty</span>()) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">items</span>.<span class="title function_">push</span>(queueElement)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">//不为空判断该插在那个优先级前面</span></span><br><span class="line">      <span class="keyword">const</span> length = <span class="variable language_">this</span>.<span class="property">items</span>.<span class="property">length</span></span><br><span class="line">      <span class="keyword">let</span> added = <span class="literal">false</span></span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; length; i++) &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">items</span>[i].<span class="property">priority</span> &gt; queueElement.<span class="property">priority</span>) &#123;</span><br><span class="line">          <span class="variable language_">this</span>.<span class="property">items</span>.<span class="title function_">splice</span>(i, <span class="number">0</span>, queueElement)</span><br><span class="line">          added = <span class="literal">true</span></span><br><span class="line">          <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// 都不符合就放最后</span></span><br><span class="line">      <span class="keyword">if</span> (!added) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">items</span>.<span class="title function_">push</span>(queueElement)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@desc</span> 出队</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@return</span> QueueElement &#123;el, priority&#125; 返回一个带优先级的对象</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">dequeue</span> (): <span class="title class_">QueueElement</span>&lt;<span class="built_in">any</span>, <span class="built_in">number</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>.<span class="title function_">shift</span>()</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@desc</span> 返回队列第一名但不改变队列</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@return</span> QueueElement &#123;el, priority&#125; 返回一个带优先级的对象</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">front</span> (): <span class="title class_">QueueElement</span>&lt;<span class="built_in">any</span>, <span class="built_in">number</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123;el, priority&#125; = <span class="variable language_">this</span>.<span class="property">items</span>[<span class="number">0</span>]</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">QueueElement</span>&lt;<span class="built_in">any</span>, <span class="built_in">number</span>&gt;(el, priority)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@desc</span> 返回队列长度</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@return</span> &#123;<span class="type">number</span>&#125;</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">size</span> (): <span class="built_in">number</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>.<span class="property">length</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@desc</span> 判断队列是否为空</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@return</span> &#123;<span class="type">boolean</span>&#125;</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">isEmpty</span> (): <span class="built_in">boolean</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> !<span class="variable language_">this</span>.<span class="property">items</span>.<span class="property">length</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> priorityQueue = <span class="keyword">new</span> <span class="title class_">PriorityQueue</span>()</span><br><span class="line">priorityQueue.<span class="title function_">enqueue</span>(<span class="string">&#x27;b&#x27;</span>, <span class="number">2</span>)</span><br><span class="line">priorityQueue.<span class="title function_">enqueue</span>(<span class="string">&#x27;b&#x27;</span>, <span class="number">2</span>)</span><br><span class="line">priorityQueue.<span class="title function_">enqueue</span>(<span class="string">&#x27;a&#x27;</span>, <span class="number">1</span>)</span><br><span class="line">priorityQueue.<span class="title function_">enqueue</span>(<span class="string">&#x27;c&#x27;</span>, <span class="number">3</span>)</span><br><span class="line">priorityQueue.<span class="title function_">enqueue</span>(<span class="string">&#x27;a&#x27;</span>, <span class="number">1</span>)</span><br><span class="line">priorityQueue.<span class="title function_">enqueue</span>(<span class="string">&#x27;a&#x27;</span>, <span class="number">1</span>)</span><br><span class="line"><span class="comment">// QueueElement &#123; element: &#x27;a&#x27;, priority: 1 &#125;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(priorityQueue.<span class="title function_">dequeue</span>())</span><br><span class="line"><span class="comment">// [</span></span><br><span class="line"><span class="comment">//  QueueElement &#123; element: &#x27;a&#x27;, priority: 1 &#125;,</span></span><br><span class="line"><span class="comment">//  QueueElement &#123; element: &#x27;a&#x27;, priority: 1 &#125;,</span></span><br><span class="line"><span class="comment">//  QueueElement &#123; element: &#x27;b&#x27;, priority: 2 &#125;,</span></span><br><span class="line"><span class="comment">//  QueueElement &#123; element: &#x27;b&#x27;, priority: 2 &#125;,</span></span><br><span class="line"><span class="comment">//  QueueElement &#123; element: &#x27;c&#x27;, priority: 3 &#125;</span></span><br><span class="line"><span class="comment">// ]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(priorityQueue.<span class="property">items</span>)</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">graph LR</span><br><span class="line"></span><br></pre></td></tr></table></figure><blockquote><p>本文参考自&lt;学习JavaScript数据结构与算法&gt;。</p></blockquote>]]>
    </content>
    <id>https://blog.wybyte.top/2022/01/07/JS%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%80-%E9%98%9F%E5%88%97/</id>
    <link href="https://blog.wybyte.top/2022/01/07/JS%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%80-%E9%98%9F%E5%88%97/"/>
    <published>2022-01-07T09:44:38.000Z</published>
    <summary>
      <![CDATA[<h1 id="队列的概念"><a href="#队列的概念" class="headerlink" title="队列的概念"></a>队列的概念</h1><p>​		队列是遵循FIFO（First In First Out，先进先出，也称为先来先服务）原则的一组有序的项。意如]]>
    </summary>
    <title>JS算法基础-队列</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="JavaScript" scheme="https://blog.wybyte.top/categories/JavaScript/"/>
    <category term="canvas" scheme="https://blog.wybyte.top/categories/JavaScript/canvas/"/>
    <category term="JavaScript" scheme="https://blog.wybyte.top/tags/JavaScript/"/>
    <category term="canvas" scheme="https://blog.wybyte.top/tags/canvas/"/>
    <content>
      <![CDATA[<h2 id="画图库"><a href="#画图库" class="headerlink" title="画图库"></a>画图库</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 手写板</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@constructor</span></span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> width 手写板宽度</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> height 手写板高度</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> lineW 线粗</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> lineColor 线颜色</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> bgColor 手写板颜色</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> El canvas节点Dom元素</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> historyLength 历史记录条数</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> dataUrl 获取base64编码</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> mime 文件类型</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> dataURLtoBlob 获取blob文件</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> dataUrl base64编码</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> filename 文件名</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> dataURLtoFile 获取file文件</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> dataUrl base64编码</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> filename 文件名</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> downLoadUrl 获取下载地址</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> dataUrl base64编码</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> filename 文件名</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> save 保存图片</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> mime 文件类型</span></span><br><span class="line"><span class="comment"> *      <span class="doctag">@param</span> filename 文件名</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> clear 清空canvas区</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> getHistory 获取操作记录</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> upStep 上一步</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> nextStep 下一步</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@method</span> clearHistory 清空历史记录</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure><p>实现了鼠标画图，自定义颜色，历史记录，上一步，下一步等功能</p><p>如图所示：</p><p><img src="https://imgs.wybyte.top/file/1635832336154.jpg" alt="示例"></p><div class="note danger flat"><p>没有写样式，只是为了展示基本功能</p></div>]]>
    </content>
    <id>https://blog.wybyte.top/2021/11/02/canvas%E5%9F%BA%E7%A1%80%E7%94%BB%E5%9B%BE%E5%BA%93/</id>
    <link href="https://blog.wybyte.top/2021/11/02/canvas%E5%9F%BA%E7%A1%80%E7%94%BB%E5%9B%BE%E5%BA%93/"/>
    <published>2021-11-02T11:44:49.000Z</published>
    <summary>
      <![CDATA[<h2 id="画图库"><a href="#画图库" class="headerlink" title="画图库"></a>画图库</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span]]>
    </summary>
    <title>canvas基础画图库</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="算法" scheme="https://blog.wybyte.top/categories/%E7%AE%97%E6%B3%95/"/>
    <category term="栈" scheme="https://blog.wybyte.top/categories/%E7%AE%97%E6%B3%95/%E6%A0%88/"/>
    <category term="JavaScript" scheme="https://blog.wybyte.top/tags/JavaScript/"/>
    <category term="算法" scheme="https://blog.wybyte.top/tags/%E7%AE%97%E6%B3%95/"/>
    <content>
      <![CDATA[<h1 id="栈的概念"><a href="#栈的概念" class="headerlink" title="栈的概念"></a>栈的概念</h1><p>​  栈是一种遵从后进先出（LIFO）原则的有序集合。新添加的或待删除的元素都保存在栈的末尾，称作栈顶，另一端就叫栈底。在栈里，新元素都靠近栈顶，旧元素都接近栈底。</p><p>​  栈作为一种数据结构，是一种只能在一端进行插入和删除操作的特殊线性表。它按照后进先出的原则存储数据，先进入的数据被压入栈底，最后的数据在栈顶，需要读数据的时候从栈顶开始弹出数据（最后一个数据被第一个读出来）。栈具有记忆作用，对栈的插入与删除操作中，不需要改变栈底指针。</p><p>​  通俗来讲，栈就好像一个箱子，我们放东西时从箱子顶部放入(入栈)，而最先放入的东西会被压到箱子底(栈底)，顺序放入依次从箱子底往上排，最后的东西在箱子口(栈顶)，需要拿东西的时候从箱子顶部依次拿(出栈)，</p><p>​  如图所示</p><p><img src="https://imgs.wybyte.top/file/20211028-1.jpeg" alt="示例"></p><h1 id="栈的创建"><a href="#栈的创建" class="headerlink" title="栈的创建"></a>栈的创建</h1><p>在原生JS对象中并没有栈这个结构的定义，所以要使用栈就要先创造它。。。</p><p>我们先声明一个栈的类，这里使用数组去存储栈内的元素</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Stack</span> &#123;</span><br><span class="line">  items</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">items</span> = []</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>栈的数据结构需要一些必要的方法</p><p>如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// push(el)  添加一个新元素到栈顶(入栈)</span></span><br><span class="line"><span class="comment">// pop()   移除栈顶元素，同时返回被移除的元素(出栈)</span></span><br><span class="line"><span class="comment">// peek()   返回栈顶元素，不对栈做任何修改</span></span><br><span class="line"><span class="comment">// isEmpty() 判断栈是否为空，空返回true</span></span><br><span class="line"><span class="comment">// clear()  移除栈内所有元素</span></span><br><span class="line"><span class="comment">// size()   返回栈内元素个数</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Stack</span> &#123;</span><br><span class="line">  items</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">items</span> = []</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 入栈</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> &#123;<span class="type">*</span>&#125; el </span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">push</span>(<span class="params">el</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">items</span>.<span class="title function_">push</span>(el)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 出栈</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@returns</span> </span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">pop</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>.<span class="title function_">pop</span>()</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 获取栈顶元素</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@returns</span> </span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">peek</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>[<span class="variable language_">this</span>.<span class="property">items</span>.<span class="property">length</span> - <span class="number">1</span>]</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 判断是否为空</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@returns</span> </span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">isEmpty</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>.<span class="property">length</span> === <span class="number">0</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 清空栈元素</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">clear</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">items</span> = []</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 获取栈内元素个数</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@returns</span> </span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">size</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">items</span>.<span class="property">length</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="栈的使用"><a href="#栈的使用" class="headerlink" title="栈的使用"></a>栈的使用</h1><h2 id="十进制转二进制"><a href="#十进制转二进制" class="headerlink" title="十进制转二进制"></a>十进制转二进制</h2><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">divideBy2</span>(<span class="params">n</span>) &#123;</span><br><span class="line">  <span class="comment">// 使用new创建stack实例</span></span><br><span class="line">  <span class="keyword">let</span> stack = <span class="keyword">new</span> <span class="title class_">Stack</span>()</span><br><span class="line">  <span class="comment">// 二进制字符串</span></span><br><span class="line">  <span class="keyword">let</span> binaryString = <span class="string">&#x27;&#x27;</span></span><br><span class="line">  <span class="keyword">while</span> (n &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 取模入栈</span></span><br><span class="line">    <span class="keyword">let</span> rem = n % <span class="number">2</span></span><br><span class="line">    stack.<span class="title function_">push</span>(rem)</span><br><span class="line">    n = <span class="title class_">Math</span>.<span class="title function_">floor</span>(n / <span class="number">2</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">while</span> (!stack.<span class="title function_">isEmpty</span>()) &#123;</span><br><span class="line">    <span class="comment">// 逐个出栈加到结果字符串</span></span><br><span class="line">    binaryString += stack.<span class="title function_">pop</span>()</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> binaryString</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>本文参考自&lt;学习JavaScript数据结构与算法&gt;,以及百度百科。</p></blockquote>]]>
    </content>
    <id>https://blog.wybyte.top/2021/10/28/JS%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%80-%E6%A0%88/</id>
    <link href="https://blog.wybyte.top/2021/10/28/JS%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%80-%E6%A0%88/"/>
    <published>2021-10-28T09:43:31.000Z</published>
    <summary>
      <![CDATA[<h1 id="栈的概念"><a href="#栈的概念" class="headerlink" title="栈的概念"></a>栈的概念</h1><p>​  栈是一种遵从后进先出（LIFO）原则的有序集合。新添加的或待删除的元素都保存在栈的末尾，称作栈顶，另一端就叫栈底。在栈]]>
    </summary>
    <title>JS算法基础-栈</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="git" scheme="https://blog.wybyte.top/categories/git/"/>
    <category term="git" scheme="https://blog.wybyte.top/tags/git/"/>
    <category term="个人笔记" scheme="https://blog.wybyte.top/tags/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<h2 id="Git第一次提交代码到仓库"><a href="#Git第一次提交代码到仓库" class="headerlink" title="Git第一次提交代码到仓库"></a>Git第一次提交代码到仓库</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 初始化git仓库</span></span><br><span class="line">$ git init </span><br><span class="line"><span class="comment"># 添加当前目录的所有文件到暂存区</span></span><br><span class="line">$ git add .</span><br><span class="line"><span class="comment"># 提交暂存区到仓库并填写注释</span></span><br><span class="line">$ git commit -m <span class="string">&#x27;注释&#x27;</span></span><br><span class="line"><span class="comment"># 添加远程仓库</span></span><br><span class="line">$ git remote add origin git仓库远程地址</span><br><span class="line"><span class="comment"># 将本地仓库推送到远程</span></span><br><span class="line">$ git push -u origin master</span><br></pre></td></tr></table></figure><h2 id="Git拉取-检出分支"><a href="#Git拉取-检出分支" class="headerlink" title="Git拉取&#x2F;检出分支"></a>Git拉取&#x2F;检出分支</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 第一次克隆远程仓库</span></span><br><span class="line">$ git init</span><br><span class="line">$ git <span class="built_in">clone</span> 远程仓库地址</span><br><span class="line"><span class="comment"># 检出远程分支到本地</span></span><br><span class="line">$ git checkout -b 本地分支名 origin/远程分支名</span><br><span class="line"><span class="comment"># 基于commit提交创建分支</span></span><br><span class="line">$ git checkout -b 新分支 <span class="built_in">hash</span>值</span><br></pre></td></tr></table></figure><h2 id="Git新建分支"><a href="#Git新建分支" class="headerlink" title="Git新建分支"></a>Git新建分支</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建本地新分支</span></span><br><span class="line">$ git branch 新分支名</span><br><span class="line"><span class="comment"># 切换本地新分支</span></span><br><span class="line">$ git checkout 新分支名</span><br><span class="line"><span class="comment"># 创建并切换到新分支</span></span><br><span class="line">$ git checkout -b 新分支名</span><br><span class="line"><span class="comment"># push到远程分支</span></span><br><span class="line">$ git push origin 新分支名:新分支名</span><br><span class="line"><span class="comment"># 检出远程分支到本地</span></span><br><span class="line">$ git checkout -b 本地分支名 origin/远程分支名</span><br><span class="line"><span class="comment"># 设置跟踪远程分支</span></span><br><span class="line">$ git branch --set-upstream-to=origin/远程分支 本地分支</span><br></pre></td></tr></table></figure><h2 id="Git删除分支"><a href="#Git删除分支" class="headerlink" title="Git删除分支"></a>Git删除分支</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看本地分支</span></span><br><span class="line">$ git branch</span><br><span class="line"><span class="comment"># 查看本地和远程分支</span></span><br><span class="line">$ git branch -a</span><br><span class="line"><span class="comment"># 删除本地分支</span></span><br><span class="line">$ git branch -d 分支名</span><br><span class="line"><span class="comment"># 删除远程分支</span></span><br><span class="line">$ git push origin --delete 分支名</span><br><span class="line"><span class="comment"># 或</span></span><br><span class="line">$ git push origin :分支名</span><br><span class="line"><span class="comment"># 清除本地无效分支</span></span><br><span class="line">$ git fetch -p</span><br></pre></td></tr></table></figure><h2 id="Git提交代码"><a href="#Git提交代码" class="headerlink" title="Git提交代码"></a>Git提交代码</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 拉取远程代码</span></span><br><span class="line">$ git pull</span><br><span class="line"><span class="comment"># 添加当前目录的所有文件到暂存区</span></span><br><span class="line">$ git add .</span><br><span class="line"><span class="comment"># 取消暂存到上一次提交，保留更改到工作区[文件名可选] --hard 将暂存区与工作区都回到上一次版本</span></span><br><span class="line">$ git reset HEAD [file]</span><br><span class="line"><span class="comment"># 提交暂存区到仓库并填写注释</span></span><br><span class="line">$ git commit -m <span class="string">&#x27;注释&#x27;</span></span><br><span class="line"><span class="comment"># 推送到远程</span></span><br><span class="line">$ git push</span><br></pre></td></tr></table></figure><h2 id="Git忽略上传文件或目录"><a href="#Git忽略上传文件或目录" class="headerlink" title="Git忽略上传文件或目录"></a>Git忽略上传文件或目录</h2><p>在.git文件夹同级新建.gitignore文件</p><p>忽略规则</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">dirName// 忽略dirName目录</span><br><span class="line">文件名（带后缀名）// 忽略指定文件</span><br><span class="line">log/*// 忽略log下的所有文件</span><br><span class="line">css/*.css// 忽略css目录下的所有.css文件</span><br></pre></td></tr></table></figure><h2 id="Git贮藏"><a href="#Git贮藏" class="headerlink" title="Git贮藏"></a>Git贮藏</h2><p>当需要checkout出新的分支又不想提交当前改动时，利用贮藏储存然后checkout 新的分支，完成工作后继续上次的工作再应用贮藏的改动。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 执行储存[备注信息可选]</span></span><br><span class="line">$ git stash [save <span class="string">&quot;save message&quot;</span>]</span><br><span class="line"><span class="comment"># 查看贮藏列表</span></span><br><span class="line">$ git stash list</span><br><span class="line"><span class="comment"># 显示做了哪些改动默认为第一个，如果显示其他后面加@&#123;$num&#125;</span></span><br><span class="line">$ git stash show [stash@&#123;1&#125;]</span><br><span class="line"><span class="comment"># 显示具体的改动</span></span><br><span class="line">$ git stash show [stash@&#123;1&#125;] -p</span><br><span class="line"><span class="comment"># 应用贮藏</span></span><br><span class="line">$ git stash apply [stash@&#123;1&#125;]</span><br><span class="line"><span class="comment"># 恢复之前的贮藏并在stash列表中删除</span></span><br><span class="line">$ git stash pop [stash@&#123;1&#125;]</span><br><span class="line"><span class="comment"># 丢弃贮藏</span></span><br><span class="line">$ git stash drop [stash@&#123;1&#125;]</span><br><span class="line"><span class="comment"># 删除所有贮藏</span></span><br><span class="line">$ git stash clear</span><br></pre></td></tr></table></figure><div class="note red no-icon flat"><p>新增的文件不在git版本控制中不会贮藏，如果需要贮藏可以先<code>git add [file]</code>再进行贮藏</p></div>]]>
    </content>
    <id>https://blog.wybyte.top/2021/09/07/git%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/</id>
    <link href="https://blog.wybyte.top/2021/09/07/git%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/"/>
    <published>2021-09-07T14:15:21.000Z</published>
    <summary>
      <![CDATA[<h2 id="Git第一次提交代码到仓库"><a href="#Git第一次提交代码到仓库" class="headerlink" title="Git第一次提交代码到仓库"></a>Git第一次提交代码到仓库</h2><figure class="highlight bash]]>
    </summary>
    <title>git基本操作</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="JavaScript" scheme="https://blog.wybyte.top/categories/JavaScript/"/>
    <category term="JavaScript" scheme="https://blog.wybyte.top/tags/JavaScript/"/>
    <category term="JS" scheme="https://blog.wybyte.top/tags/JS/"/>
    <category term="深拷贝" scheme="https://blog.wybyte.top/tags/%E6%B7%B1%E6%8B%B7%E8%B4%9D/"/>
    <content>
      <![CDATA[<h2 id="赋值、深拷贝、浅拷贝区别"><a href="#赋值、深拷贝、浅拷贝区别" class="headerlink" title="赋值、深拷贝、浅拷贝区别"></a>赋值、深拷贝、浅拷贝区别</h2><h3 id="赋值"><a href="#赋值" class="headerlink" title="赋值"></a>赋值</h3><p>赋值是某一个<mark class="hl-label blue">数值</mark>或<mark class="hl-label blue">变量</mark>赋给某个<mark class="hl-label blue">变量</mark>的过程，分为：</p><ul><li>基本数据类型：赋值后两个变量的值互相没有关联关系，改变其一不会引起另一个变量值变化。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> a = <span class="string">&#x27;abc&#x27;</span></span><br><span class="line"><span class="keyword">let</span> b = a</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#x27;abc&#x27;</span></span><br><span class="line">a = <span class="string">&#x27;def&#x27;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a) <span class="comment">// &#x27;def&#x27;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#x27;abc&#x27;</span></span><br></pre></td></tr></table></figure><blockquote><p>基本数据类型赋值时会在栈内存中创建一个新值。</p></blockquote><ul><li>引用数据类型：普通赋值操作使两个变量具有相同引用，指向同一个对象，改变任何一个，另一个取到的都会是改变后的值。</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> a = &#123;</span><br><span class="line"> <span class="attr">name</span>: <span class="string">&#x27;abc&#x27;</span>,</span><br><span class="line"> <span class="attr">age</span>: <span class="number">20</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">let</span> b = a</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#123;name: &quot;abc&quot;, age: 20&#125;</span></span><br><span class="line">a.<span class="property">name</span> = <span class="string">&#x27;def&#x27;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a) <span class="comment">// &#123;name: &quot;def&quot;, age: 20&#125;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#123;name: &quot;def&quot;, age: 20&#125;</span></span><br></pre></td></tr></table></figure><blockquote><p>引用类型赋值时会在栈内存中存储引用对象的指针，真实对象在堆内存中。</p><p>想要得到两个相同值的引用类型的数据，又不想让它们指向同一个对象就需要用到深拷贝。</p></blockquote><h3 id="浅拷贝"><a href="#浅拷贝" class="headerlink" title="浅拷贝"></a>浅拷贝</h3><p>浅拷贝只是复制一层对象，当对象的属性是引用类型时，实际复制的是该属性的引用地址，当引用地址对应的值改变时，其他值为该引用地址的变量都会改变。</p><h4 id="浅拷贝-直接赋值"><a href="#浅拷贝-直接赋值" class="headerlink" title="浅拷贝-直接赋值"></a>浅拷贝-直接赋值</h4><p>该方法只能拷贝基本数据类型的值，引用类型的值第一层属性就会变成拷贝引用地址</p><blockquote><p>示例见赋值操作</p></blockquote><h4 id="浅拷贝-Object-assign"><a href="#浅拷贝-Object-assign" class="headerlink" title="浅拷贝-Object.assign()"></a>浅拷贝-<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#%E6%8F%8F%E8%BF%B0">Object.assign()</a></h4><p><code>**Object.assign()**</code> 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。</p><p>它会拷贝浅层引用类型对象的属性，<code>只会拷贝对象的第一层属性，对象内的引用类型属性只会拷贝引用地址</code></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> a = &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;abc&#x27;</span>,</span><br><span class="line">    <span class="attr">shop</span>: &#123;</span><br><span class="line">        <span class="attr">price</span>: <span class="number">100</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> b = <span class="title class_">Object</span>.<span class="title function_">assign</span>(&#123;&#125;, a)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#123;name: &quot;abc&quot;, shop: &#123;price: 100&#125;&#125;</span></span><br><span class="line"><span class="comment">// 当a的值改变时</span></span><br><span class="line">a.<span class="property">name</span> = <span class="string">&#x27;def&#x27;</span> <span class="comment">// 第一层属性</span></span><br><span class="line">a.<span class="property">shop</span>.<span class="property">price</span> = <span class="number">200</span> <span class="comment">// 第二层属性</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a) <span class="comment">// &#123;name: &quot;def&quot;, shop: &#123;price: 200&#125;&#125;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#123;name: &quot;abc&quot;, shop: &#123;price: 200&#125;&#125;</span></span><br></pre></td></tr></table></figure><div class="note red no-icon flat"><p>当改变a的第二层属性时，b的属性值也改变了</p></div><h4 id="浅拷贝-‘…‘扩展运算符"><a href="#浅拷贝-‘…‘扩展运算符" class="headerlink" title="浅拷贝-‘…‘扩展运算符"></a>浅拷贝-‘<a href="https://es6.ruanyifeng.com/#docs/array#%E6%89%A9%E5%B1%95%E8%BF%90%E7%AE%97%E7%AC%A6">…</a>‘扩展运算符</h4><p>扩展运算符（spread）是三个点（<code>...</code>）。它好比 rest 参数的逆运算，将一个数组转为用逗号分隔的参数序列。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> a = &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;abc&#x27;</span>,</span><br><span class="line">    <span class="attr">shop</span>: &#123;</span><br><span class="line">        <span class="attr">price</span>: <span class="number">100</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> b = &#123;...a&#125;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#123;name: &quot;abc&quot;, shop: &#123;price: 100&#125;&#125;</span></span><br><span class="line">a.<span class="property">name</span> = <span class="string">&#x27;def&#x27;</span></span><br><span class="line">a.<span class="property">shop</span>.<span class="property">price</span> = <span class="number">200</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a) <span class="comment">// &#123;name: &quot;def&quot;, shop: &#123;price: 200&#125;&#125;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#123;name: &quot;abc&quot;, shop: &#123;price: 200&#125;&#125;</span></span><br></pre></td></tr></table></figure><div class="note red no-icon flat"><p>同Object.assign()一样，当改变a的第二层属性时，b的属性值也改变了</p></div><h4 id="浅拷贝-Array-slice-方法"><a href="#浅拷贝-Array-slice-方法" class="headerlink" title="浅拷贝-Array.slice()方法"></a>浅拷贝-<a href="https://www.w3school.com.cn/js/jsref_slice_array.asp">Array.slice()</a>方法</h4><p><code>slice(start [,end])</code>方法不会改变原数组，会返回一个新的数组，包含从 start 到 end （不包括该元素）的元素。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> a = [<span class="number">0</span>, <span class="string">&#x27;1&#x27;</span>, [<span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>]]</span><br><span class="line"><span class="keyword">let</span> b = a.<span class="title function_">slice</span>(<span class="number">0</span>)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// [0, &quot;1&quot;, [2, 3, 4]]</span></span><br><span class="line">a[<span class="number">0</span>] = <span class="number">123</span></span><br><span class="line">a[<span class="number">2</span>][<span class="number">0</span>] = <span class="number">234</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a) <span class="comment">// [123, &quot;1&quot;, [234, 3, 4]]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// [0, &quot;1&quot;, [234, 3, 4]]</span></span><br></pre></td></tr></table></figure><div class="note red no-icon flat"><p>这个方法只能浅拷贝数组使用</p></div><h3 id="深拷贝"><a href="#深拷贝" class="headerlink" title="深拷贝"></a>深拷贝</h3><p>深拷贝会拷贝对象的所有属性，相当于创建一个新的对象，深拷贝的对象与原对象除了值相同外不存在互相的引用关系，当改变一个对象属性时另一个不会发生变化，深拷贝相对于浅拷贝开销较大，拷贝前后对象互不影响。</p><h4 id="深拷贝-JSON-parse-JSON-stringify"><a href="#深拷贝-JSON-parse-JSON-stringify" class="headerlink" title="深拷贝-JSON.parse()  JSON.stringify()"></a>深拷贝-<a href="https://www.w3school.com.cn/js/js_json_parse.asp">JSON.parse()</a>  <a href="https://www.w3school.com.cn/js/js_json_stringify.asp">JSON.stringify()</a></h4><p>JSON.parse(JSON.stringify(Object))，原理是将引用类型对象转换成JSON字符串，再将字符串转换为一个对象，这时返回的对象会成为一个新的引用类型对象。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> a = &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;abc&#x27;</span>,</span><br><span class="line">    <span class="attr">shop</span>: &#123;</span><br><span class="line">        <span class="attr">price</span>: <span class="number">100</span>,</span><br><span class="line">        <span class="attr">title</span>: <span class="literal">undefined</span>,</span><br><span class="line">        <span class="attr">avatar</span>: <span class="literal">null</span>,</span><br><span class="line">        <span class="attr">read</span>: <span class="keyword">function</span>(<span class="params"></span>)&#123;&#125;,</span><br><span class="line">       <span class="attr">symbol</span>: <span class="title class_">Symbol</span>(<span class="string">&#x27;bcd&#x27;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> b = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(a))</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#123;name: &quot;abc&quot;, shop: &#123;price: 100, avatar: null&#125;&#125;</span></span><br><span class="line"><span class="comment">// 当a的值改变时</span></span><br><span class="line">a.<span class="property">name</span> = <span class="string">&#x27;def&#x27;</span> <span class="comment">// 第一层属性</span></span><br><span class="line">a.<span class="property">shop</span>.<span class="property">price</span> = <span class="number">200</span> <span class="comment">// 第二层属性</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a) <span class="comment">// &#123;name: &quot;def&quot;, shop: &#123;price: 200, title: undefined, avatar: null, read: ƒ (), symbol: Symbol(bcd)&#125;&#125;</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(b) <span class="comment">// &#123;name: &quot;abc&quot;, shop: &#123;price: 100, avatar: null&#125;&#125;</span></span><br></pre></td></tr></table></figure><div class="note danger flat"><p>这个方法拷贝对象时，会有以下问题：</p><ul><li>会忽略 <code>undefined</code></li><li>会忽略 <code>symbol</code></li><li>会忽略函数</li><li>不能解决循环引用的对象(会报错Uncaught TypeError: Converting circular structure to JSON)</li><li>不能正确处理<code>new Date()</code>（会将时间对象转换为字符串时间）</li><li>不能处理正则</li></ul></div><h4 id="深拷贝-递归"><a href="#深拷贝-递归" class="headerlink" title="深拷贝-递归"></a>深拷贝-递归</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">deepCopy</span>(<span class="params"> source </span>) &#123;</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">typeof</span> source !== <span class="string">&#x27;object&#x27;</span>) <span class="keyword">return</span> source; <span class="comment">//如果不是对象的话直接返回</span></span><br><span class="line">  <span class="keyword">let</span> target = <span class="title class_">Array</span>.<span class="title function_">isArray</span>( source ) ? [] : &#123;&#125; <span class="comment">//数组兼容</span></span><br><span class="line">  <span class="keyword">for</span> ( <span class="keyword">var</span> k <span class="keyword">in</span> source ) &#123;</span><br><span class="line">    <span class="keyword">if</span> (source.<span class="title function_">hasOwnProperty</span>(k)) &#123;</span><br><span class="line">    <span class="keyword">if</span> ( <span class="keyword">typeof</span> source[ k ] === <span class="string">&#x27;object&#x27;</span> ) &#123;</span><br><span class="line">        target[ k ] = <span class="title function_">deepCopy</span>( source[ k ] )</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        target[ k ] = source[ k ]</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> target</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="深拷贝-获取所有属性逐个添加"><a href="#深拷贝-获取所有属性逐个添加" class="headerlink" title="深拷贝-获取所有属性逐个添加"></a>深拷贝-获取所有属性逐个添加</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">copyObject</span>(<span class="params">orig</span>) &#123;</span><br><span class="line">   <span class="comment">// Object.create(prototype) 使用原型创建新的对象</span></span><br><span class="line">   <span class="comment">// Object.getPrototypeOf(obj) 返回对象的原型</span></span><br><span class="line">    <span class="keyword">var</span> copy = <span class="title class_">Object</span>.<span class="title function_">create</span>(<span class="title class_">Object</span>.<span class="title function_">getPrototypeOf</span>(orig));</span><br><span class="line">    <span class="title function_">copyOwnPropertiesFrom</span>(copy, orig);</span><br><span class="line">    <span class="keyword">return</span> copy;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">  <span class="keyword">function</span> <span class="title function_">copyOwnPropertiesFrom</span>(<span class="params">target, source</span>) &#123;</span><br><span class="line">    <span class="comment">// Object.getOwnPropertyNames 返回指定对象的所有属性名组成的数组（包含不可枚举属性）</span></span><br><span class="line">    <span class="comment">// Object.getOwnPropertyDescriptor 返回对象上属性对应的属性描述符</span></span><br><span class="line">    <span class="comment">// Object.defineProperty 直接在对象上定义一个新属性，或者修改一个现有属性</span></span><br><span class="line">    <span class="title class_">Object</span></span><br><span class="line">    .<span class="title function_">getOwnPropertyNames</span>(source)</span><br><span class="line">    .<span class="title function_">forEach</span>(<span class="keyword">function</span> (<span class="params">propKey</span>) &#123;</span><br><span class="line">      <span class="keyword">var</span> desc = <span class="title class_">Object</span>.<span class="title function_">getOwnPropertyDescriptor</span>(source, propKey);</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">typeof</span> desc.<span class="property">value</span> === <span class="string">&#x27;object&#x27;</span>) &#123;</span><br><span class="line">        target[propKey] = <span class="title function_">copyObject</span>(source[propKey]);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="title class_">Object</span>.<span class="title function_">defineProperty</span>(target, propKey, desc);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="keyword">return</span> target;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><blockquote><p>后两个方法原文地址： <a href="https://www.cnblogs.com/dobeco/p/11295316.html">https://www.cnblogs.com/dobeco/p/11295316.html</a><br>方法参考链接：</p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create">https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create</a></p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf">https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf</a></p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames">https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames</a></p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor">https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor</a></p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty">https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty</a></p></blockquote>]]>
    </content>
    <id>https://blog.wybyte.top/2021/08/18/JS%E6%B7%B1%E6%8B%B7%E8%B4%9D/</id>
    <link href="https://blog.wybyte.top/2021/08/18/JS%E6%B7%B1%E6%8B%B7%E8%B4%9D/"/>
    <published>2021-08-18T14:21:48.000Z</published>
    <summary>
      <![CDATA[<h2 id="赋值、深拷贝、浅拷贝区别"><a href="#赋值、深拷贝、浅拷贝区别" class="headerlink" title="赋值、深拷贝、浅拷贝区别"></a>赋值、深拷贝、浅拷贝区别</h2><h3 id="赋值"><a href="#赋值" class="]]>
    </summary>
    <title>JS深拷贝</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>Wang</name>
    </author>
    <category term="hexo" scheme="https://blog.wybyte.top/categories/hexo/"/>
    <category term="Hexo练习" scheme="https://blog.wybyte.top/tags/Hexo%E7%BB%83%E4%B9%A0/"/>
    <category term="Typora" scheme="https://blog.wybyte.top/tags/Typora/"/>
    <content>
      <![CDATA[<h2 id="Typora"><a href="#Typora" class="headerlink" title="Typora"></a>Typora</h2><p>一款编写Markdown的软件</p><h3 id="常用快捷键"><a href="#常用快捷键" class="headerlink" title="常用快捷键"></a>常用快捷键</h3><p>标题： Ctrl&#x2F;Command + 1、2、3…对应一、二、三级标题</p><p><strong>加粗</strong>：Ctrl&#x2F;Command + B</p><p><em>斜体</em>：Ctrl&#x2F;Command + I</p><p><u>下划线</u>：Ctrl&#x2F;Command + U</p><p><del>删除线</del>：control + shift + &#96;</p><p><code>代码片段</code>：control + &#96;</p><p>代码块：option + command + c</p><p><a href="">链接</a>：command + k</p><p>取消格式：再按相同快捷键</p><p>有序列表：数字+点+空格</p><p>任务列表： +&#x2F;-加空格</p><p>嵌套列表：Tab</p><p>插入表格：option + command + t</p><p>引用：输入&gt;加空格，或者option + command + q</p><h2 id="Hexo"><a href="#Hexo" class="headerlink" title="Hexo"></a>Hexo</h2><h3 id="创建一个md文件"><a href="#创建一个md文件" class="headerlink" title="创建一个md文件"></a>创建一个md文件</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hexo new &lt;title&gt;</span><br><span class="line">hexo new <span class="string">&#x27;标题&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="布局（layout）"><a href="#布局（layout）" class="headerlink" title="布局（layout）"></a>布局（layout）</h3><ul><li>创建md时，我们可以指定布局</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hexo new [layout] &lt;title&gt;</span><br><span class="line">hexo new page <span class="string">&#x27;我的页面&#x27;</span></span><br></pre></td></tr></table></figure><ul><li><p>布局有三种：<code>post</code>（文章）、<code>draft</code>（草稿）、<code>page</code>（页面）</p><p>在新建文件时，Hexo会根据<code>scaffolds</code>文件对应的文件来建立md</p></li></ul><p><img src="https://imgs.wybyte.top/file/20210817111500.jpg" alt="示例"></p><ul><li>如果没有指定默认为<code>post</code>，可以在站点配置文件修改<code>default_layout</code>参数来修改默认布局。</li></ul><blockquote><p>对于独立页面来说，Hexo会创建一个以标题为名字的文件</p></blockquote><h3 id="草稿（draft）"><a href="#草稿（draft）" class="headerlink" title="草稿（draft）"></a>草稿（draft）</h3><p><code>draft</code>会被保存到<code>source/_drafts</code>文件夹中，但不会显示在页面上，如果不想文章显示在页面上，可以放到<code>_drafts</code>文件夹中。</p><ul><li>可以使用 <code>--draft</code> 参数查看草稿</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo server --draft</span><br></pre></td></tr></table></figure><ul><li><p>还可以在站点配置文件把<code>render_drafts</code>参数设为<code>true</code>来预览草稿</p></li><li><p>可以通过<code>publish</code>指令将草稿发布文章或页面，他将会被转移到指定文件夹</p></li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo publish [layout] &lt;title&gt;</span><br></pre></td></tr></table></figure><h3 id="Fornt-matter"><a href="#Fornt-matter" class="headerlink" title="Fornt-matter"></a>Fornt-matter</h3><p>当创建一个md文件后，最上方的<code>---</code>分割的区域，用于指定个别文件的变量。</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">title: Hexo新建文章</span><br><span class="line"><span class="section">date: 2021-08-17 09:30:25</span></span><br><span class="line"><span class="section">---</span></span><br></pre></td></tr></table></figure><blockquote><p>在Typora中首行输入<code>---</code>，然后回车就可以输入<code>Fornt-matter</code>了。</p></blockquote><p>Front-matter预定参数</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">layout  布局  默认为true，如果你不想你的文章被处理，可以设置为false</span><br><span class="line">title  标题  标题会显示在最上方居中位置     </span><br><span class="line">date  建立日期    如果不指定则为默认值-文件创建日期，可以自定义。</span><br><span class="line">update  更新日期  如果不指定则为默认值-文件修改后重新生成静态文件的日期。</span><br><span class="line">comments  是否开启文章的评论功能 默认值为true</span><br><span class="line">tags  标签（不适用于页面page布局）</span><br><span class="line">categoreies  分类（不适用于页面page布局）</span><br><span class="line">permalink  覆盖文章网址</span><br><span class="line">keywords  仅用于 meta 标签和 Open Graph 的关键词（不推荐使用）</span><br></pre></td></tr></table></figure><h3 id="为文章添加分类标签"><a href="#为文章添加分类标签" class="headerlink" title="为文章添加分类标签"></a>为文章添加分类标签</h3><p>只有文章（post布局）支持分类和标签，需要在<code>Front-matter</code>中设置。分类有层级关系，标签没有。</p><p>举个例子：<br>1）下面文章它的标签是：Hexo、博客<br>2）分类是： 个人博客 &gt; Hexo博客<br>3）“Hexo博客” 是 “个人博客” 的子分类</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">categories:</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">个人博客（第一层级）</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">Hexo博客（第二层级）</span></span><br><span class="line"><span class="attr">tags:</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">Hexo</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">博客</span></span><br></pre></td></tr></table></figure><p>为文章添加多个分类</p><p>1）下面文章属于三个分类：日常 &gt; 生活，日常 &gt; 随想，日记<br>2）其中生活、随想为日常的子分类，日常和日记为同级分类</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">categories:</span></span><br><span class="line"><span class="bullet">-</span> [<span class="string">日常</span>, <span class="string">生活</span>]</span><br><span class="line"><span class="bullet">-</span> [<span class="string">日常</span>, <span class="string">随想</span>]</span><br><span class="line"><span class="bullet">-</span> [<span class="string">日记</span>]</span><br></pre></td></tr></table></figure><h3 id="基本操作"><a href="#基本操作" class="headerlink" title="基本操作"></a>基本操作</h3><h4 id="常用命令"><a href="#常用命令" class="headerlink" title="常用命令"></a>常用命令</h4><ul><li><p>清除缓存：<code>hexo clean</code></p></li><li><p>生成静态文件：<code>hexo generate</code>可简写为 <code>hexo g</code></p></li><li><p>启动服务器：<code>hexo server</code>或者 <code>hexo s</code> 常用参数：<code>-p（--port）</code>重设端口</p></li><li><p>部署：<code>hexo deploy</code>可简写为<code>hexo d</code>，用于将网站部署到服务器上。（暂时用不到，目前都是在本地，后面我们将博客托管到<code>GitHub Pages</code>或<code>Gitee Pages</code>时才会用到此命令）<br>常用参数：<code>-g（--generate）</code>，<code>hexo d -g</code>部署前预先生成静态文件，等同于 <code>hexo g -d</code></p></li></ul>]]>
    </content>
    <id>https://blog.wybyte.top/2021/08/17/Hexo%E6%96%B0%E5%BB%BA%E6%96%87%E7%AB%A0/</id>
    <link href="https://blog.wybyte.top/2021/08/17/Hexo%E6%96%B0%E5%BB%BA%E6%96%87%E7%AB%A0/"/>
    <published>2021-08-17T09:30:25.000Z</published>
    <summary>
      <![CDATA[<h2 id="Typora"><a href="#Typora" class="headerlink" title="Typora"></a>Typora</h2><p>一款编写Markdown的软件</p>
<h3 id="常用快捷键"><a href="#常用快捷键" cl]]>
    </summary>
    <title>Hexo新建文章</title>
    <updated>2026-09-28T02:13:28.501Z</updated>
  </entry>
</feed>
