<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>wustxing&#39;s blog</title>
  
  <subtitle>星的博客</subtitle>
  <link href="http://blog.ypintao.com/atom.xml" rel="self"/>
  
  <link href="http://blog.ypintao.com/"/>
  <updated>2026-06-01T00:43:59.361Z</updated>
  <id>http://blog.ypintao.com/</id>
  
  <author>
    <name>wustxing</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>安卓模拟器设置</title>
    <link href="http://blog.ypintao.com/2026/06/01/%E5%89%8D%E7%AB%AF/%E9%80%86%E5%90%91/%E6%8A%93%E5%8C%85/"/>
    <id>http://blog.ypintao.com/2026/06/01/%E5%89%8D%E7%AB%AF/%E9%80%86%E5%90%91/%E6%8A%93%E5%8C%85/</id>
    <published>2026-05-31T20:00:00.000Z</published>
    <updated>2026-06-01T00:43:59.361Z</updated>
    
    <content type="html"><![CDATA[<p>1、获取mumu模拟器最新版<br>2、开启root权限，和磁盘共享 可写系统盘权限<br>点击图上三个点，然后点击设备设置<br><img src="/img/zhuabao/1.png"><br><img src="/img/zhuabao/2.png"></p><p>开启root权限，和可写系统盘</p><span id="more"></span><h2 id="Charles安装与环境配置"><a href="#Charles安装与环境配置" class="headerlink" title="Charles安装与环境配置"></a>Charles安装与环境配置</h2><h3 id="1-获取并安装Charles"><a href="#1-获取并安装Charles" class="headerlink" title="1. 获取并安装Charles"></a>1. 获取并安装Charles</h3><p>前往Charles官网获取最新版本安装包<a href="https://www.charlesproxy.com/">&gt;&gt;点击访问</a></p><h3 id="2-修改代理端口（Port）"><a href="#2-修改代理端口（Port）" class="headerlink" title="2. 修改代理端口（Port）"></a>2. 修改代理端口（Port）</h3><p>安装完成后，在Charles主页面点击顶部菜单栏— Proxy（代理）— Proxy Settings（代理设置），设置Port（代理端口号）为8888；<br><img src="/img/zhuabao/3.png"><br><img src="/img/zhuabao/4.png"></p><h3 id="3-修改SSL代理"><a href="#3-修改SSL代理" class="headerlink" title="3. 修改SSL代理"></a>3. 修改SSL代理</h3><p>返回Charles主页面，点击顶部菜单栏— Proxy（代理）— SSL Proxying Settings（SSL代理设置），勾选启用SSL代理，添加主机和端口都为*，并保存；<br><img src="/img/zhuabao/5.png"></p><h3 id="4-安装证书"><a href="#4-安装证书" class="headerlink" title="4. 安装证书"></a>4. 安装证书</h3><p>返回Charles主页面，点击顶部菜单栏— Help— SSL Proxying — Install Charles Root Certificate，安装证书，完成后重启Charles即可。</p><p><img src="/img/zhuabao/6.png"></p><p><img src="/img/zhuabao/7.png"></p><p><img src="/img/zhuabao/8.png"></p><p><img src="/img/zhuabao/9.png"><br><img src="/img/zhuabao/10.png"><br><img src="/img/zhuabao/11.png"></p><h2 id="四、模拟器证书安装流程"><a href="#四、模拟器证书安装流程" class="headerlink" title="四、模拟器证书安装流程"></a>四、模拟器证书安装流程</h2><h3 id="1-保存-pem文件到本地"><a href="#1-保存-pem文件到本地" class="headerlink" title="1. 保存.pem文件到本地"></a>1. 保存.pem文件到本地</h3><p>在Charles主页面点击顶部菜单栏—Help— Save Chales Certificate—保存到本地，如保存为Charles.pem；<br><img src="/img/zhuabao/12.png"></p><h3 id="2-打开命令提示符界面执行命令"><a href="#2-打开命令提示符界面执行命令" class="headerlink" title="2. 打开命令提示符界面执行命令"></a>2. 打开命令提示符界面执行命令</h3><p>进入Charles.pem所在磁盘目录下，点击磁盘地址栏-输入cmd并回车，在弹出的cmd界面（命令提示符界面）内，输入以下命令：</p><p>openssl x509 -inform PEM -subject_hash_old -in charles.pem</p><p>若输出结果第一行类似：d0c556f7，则将Charles.pem更名为 d0c556f7.0；</p><p>注：这个已更名的证书可以重复使用（过期后再重新生成即可），可以存放在方便的位置以便后续使用。<br><img src="/img/zhuabao/13.png"><br><img src="/img/zhuabao/14.png"></p><p>请注意，若没有安装OpenSSL，可到官网下载安装<a href="https://slproweb.com/products/Win32OpenSSL.html">&gt;&gt;点击访问</a></p><p>安装时OpenSSL选择默认安装，最后的捐款取消勾选。之后配置环境变量，将OpenSSL安装路径bin的路径(例如 C:\OpenSSL-Win64\bin)加入到操作系统的系统环境变量Path中。<br><img src="/img/zhuabao/15.png"></p><h3 id="3-获取IP地址端口-IP-Address"><a href="#3-获取IP地址端口-IP-Address" class="headerlink" title="3. 获取IP地址端口(IP Address)"></a>3. 获取IP地址端口(IP Address)</h3><p>返回Charles主页面，点击顶部菜单栏Help —Local IP Address，获取到IP Address（IP 地址端口）；<br><img src="/img/zhuabao/16.png"></p><h3 id="4-调整模拟器内WiFi代理设置"><a href="#4-调整模拟器内WiFi代理设置" class="headerlink" title="4. 调整模拟器内WiFi代理设置"></a>4. 调整模拟器内WiFi代理设置</h3><p>返回模拟器主页面，点击桌面系统应用—设置—网络与互联网—互联网，点击WiFi右侧设置，进入WiFi详情页面并点击右上角编辑，打开高级选项，将代理切换为手动，代理主机名填入Charles获取到的IP地址，代理端口填入8888之后保存；<br><img src="/img/zhuabao/17.png"><br><img src="/img/zhuabao/18.png"><br><img src="/img/zhuabao/19.png"></p><p>5.导入证书<br><img src="/img/zhuabao/20.png"></p><p>把这个放到共享文件夹下，这样模拟器上就可以看到了<br><img src="/img/zhuabao/21.png"><br>这里还需要有个工具,mumu模拟器自带的。下载安装一下就可以了<br><img src="/img/zhuabao/22.png"></p><p><img src="/img/zhuabao/23.png"></p><p>默认系统是只读的。这里可以挂载为可读写。<br><img src="/img/zhuabao/24.png"><br><img src="/img/zhuabao/25.png"></p><p>把共享的 这个证书文件复制到根目录的<br>&#x2F;system&#x2F;etc&#x2F;security&#x2F;cacerts 这个目录下<br><img src="/img/zhuabao/26.png"><br>安卓的证书就安装完毕了</p><p>接下来就访问安卓浏览器就可以看到<br><img src="/img/zhuabao/27.png"> </p>]]></content>
    
    
    <summary type="html">&lt;p&gt;1、获取mumu模拟器最新版&lt;br&gt;2、开启root权限，和磁盘共享 可写系统盘权限&lt;br&gt;点击图上三个点，然后点击设备设置&lt;br&gt;&lt;img src=&quot;/img/zhuabao/1.png&quot;&gt;&lt;br&gt;&lt;img src=&quot;/img/zhuabao/2.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;开启root权限，和可写系统盘&lt;/p&gt;</summary>
    
    
    
    <category term="逆向" scheme="http://blog.ypintao.com/categories/%E9%80%86%E5%90%91/"/>
    
    
    <category term="逆向" scheme="http://blog.ypintao.com/tags/%E9%80%86%E5%90%91/"/>
    
  </entry>
  
  <entry>
    <title>中间人攻击（MITM）原理与抓包分析</title>
    <link href="http://blog.ypintao.com/2026/06/01/%E5%89%8D%E7%AB%AF/%E9%80%86%E5%90%91/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB/"/>
    <id>http://blog.ypintao.com/2026/06/01/%E5%89%8D%E7%AB%AF/%E9%80%86%E5%90%91/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB/</id>
    <published>2026-05-31T16:00:00.000Z</published>
    <updated>2026-06-01T04:43:18.572Z</updated>
    
    <content type="html"><![CDATA[<h1 id="中间人攻击（MITM）原理与抓包分析"><a href="#中间人攻击（MITM）原理与抓包分析" class="headerlink" title="中间人攻击（MITM）原理与抓包分析"></a>中间人攻击（MITM）原理与抓包分析</h1><h2 id="一、什么是中间人攻击"><a href="#一、什么是中间人攻击" class="headerlink" title="一、什么是中间人攻击"></a>一、什么是中间人攻击</h2><p>中间人攻击（Man-in-the-Middle Attack，简称 MITM）是一种网络攻击手段，攻击者秘密地拦截并可能篡改双方之间的通信，而通信双方对此毫不知情。</p><p>简单来说，攻击者把自己”插”在了客户端和服务端之间，充当一个透明的中转站。</p><hr><h2 id="二、正常通信-vs-中间人攻击"><a href="#二、正常通信-vs-中间人攻击" class="headerlink" title="二、正常通信 vs 中间人攻击"></a>二、正常通信 vs 中间人攻击</h2><h3 id="2-1-正常通信流程"><a href="#2-1-正常通信流程" class="headerlink" title="2.1 正常通信流程"></a>2.1 正常通信流程</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></pre></td><td class="code"><pre><span class="line">┌──────────┐                        ┌──────────┐</span><br><span class="line">│          │      请求 (Request)     │          │</span><br><span class="line">│  客户端   │ ──────────────────────► │  服务端   │</span><br><span class="line">│ (Client) │                        │ (Server) │</span><br><span class="line">│          │ ◄────────────────────── │          │</span><br><span class="line">└──────────┘      响应 (Response)     └──────────┘</span><br></pre></td></tr></table></figure><p>在正常通信中，客户端直接与服务端建立连接，数据在两者之间直接传输。</p><h3 id="2-2-中间人攻击流程"><a href="#2-2-中间人攻击流程" class="headerlink" title="2.2 中间人攻击流程"></a>2.2 中间人攻击流程</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></pre></td><td class="code"><pre><span class="line">┌──────────┐                        ┌──────────┐</span><br><span class="line">│          │      请求 (Request)     │          │</span><br><span class="line">│  客户端   │ ──────────────────────► │  服务端   │</span><br><span class="line">│ (Client) │                        │ (Server) │</span><br><span class="line">│          │ ◄────────────────────── │          │</span><br><span class="line">└─────┬────┘      响应 (Response)     └─────▲────┘</span><br><span class="line">      │                                      │</span><br><span class="line">      │    ┌──────────────────────┐          │</span><br><span class="line">      │    │                      │          │</span><br><span class="line">      └───►│   中间人 (Attacker)   │──────────┘</span><br><span class="line">           │                      │</span><br><span class="line">           │  窃听 / 篡改 / 伪造   │</span><br><span class="line">           └──────────────────────┘</span><br></pre></td></tr></table></figure><p>攻击者在客户端和服务端之间拦截通信：</p><ul><li>客户端以为自己在和真正的服务端通信</li><li>服务端以为自己在和真正的客户端通信</li><li>中间人可以<strong>窃听</strong>、<strong>篡改</strong>甚至<strong>伪造</strong>通信内容</li></ul><hr><h2 id="三、HTTPS-下的中间人攻击原理"><a href="#三、HTTPS-下的中间人攻击原理" class="headerlink" title="三、HTTPS 下的中间人攻击原理"></a>三、HTTPS 下的中间人攻击原理</h2><p>HTTPS 本身就是为了防止中间人攻击而设计的，那中间人攻击是如何突破 HTTPS 的呢？</p><h3 id="3-1-正常-HTTPS-握手流程"><a href="#3-1-正常-HTTPS-握手流程" class="headerlink" title="3.1 正常 HTTPS 握手流程"></a>3.1 正常 HTTPS 握手流程</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></pre></td><td class="code"><pre><span class="line">Client                                Server</span><br><span class="line">  │                                      │</span><br><span class="line">  │  1. ClientHello                      │</span><br><span class="line">  │  (支持的加密套件、随机数)               │</span><br><span class="line">  │ ──────────────────────────────────────►</span><br><span class="line">  │                                      │</span><br><span class="line">  │  2. ServerHello + 证书                │</span><br><span class="line">  │  (选定加密套件、服务端证书)              │</span><br><span class="line">  │ ◄──────────────────────────────────────</span><br><span class="line">  │                                      │</span><br><span class="line">  │  3. 客户端验证证书                     │</span><br><span class="line">  │  (CA 签名验证、域名匹配、有效期检查)     │</span><br><span class="line">  │                                      │</span><br><span class="line">  │  4. 生成 Pre-Master Secret            │</span><br><span class="line">  │  用服务端公钥加密发送                    │</span><br><span class="line">  │ ──────────────────────────────────────►</span><br><span class="line">  │                                      │</span><br><span class="line">  │  5. 双方生成会话密钥                    │</span><br><span class="line">  │  (Session Key)                       │</span><br><span class="line">  │                                      │</span><br><span class="line">  │  6. 加密通信开始                       │</span><br><span class="line">  │ ◄─────────────────────────────────────►</span><br></pre></td></tr></table></figure><p>关键点：客户端会验证服务端证书是否由<strong>可信 CA 机构</strong>签发，如果证书不可信，浏览器会弹出警告。</p><h3 id="3-2-中间人攻击-HTTPS-的方式"><a href="#3-2-中间人攻击-HTTPS-的方式" class="headerlink" title="3.2 中间人攻击 HTTPS 的方式"></a>3.2 中间人攻击 HTTPS 的方式</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></pre></td><td class="code"><pre><span class="line">Client              MITM(Attacker)              Server</span><br><span class="line">  │                      │                        │</span><br><span class="line">  │  1. ClientHello      │                        │</span><br><span class="line">  │ ────────────────────►│  1&#x27;. ClientHello       │</span><br><span class="line">  │                      │ ──────────────────────►│</span><br><span class="line">  │                      │                        │</span><br><span class="line">  │                      │  2&#x27;. ServerHello+真证书  │</span><br><span class="line">  │  2. ServerHello      │ ◄──────────────────────│</span><br><span class="line">  │  + 伪造证书           │                        │</span><br><span class="line">  │ ◄────────────────────│                        │</span><br><span class="line">  │                      │                        │</span><br><span class="line">  │  3. 客户端验证伪造证书  │                        │</span><br><span class="line">  │  (需要信任伪造CA)      │                        │</span><br><span class="line">  │                      │                        │</span><br><span class="line">  │  4. 用伪造公钥加密     │                        │</span><br><span class="line">  │ ────────────────────►│  4&#x27;. 用真公钥加密        │</span><br><span class="line">  │                      │ ──────────────────────►│</span><br><span class="line">  │                      │                        │</span><br><span class="line">  │  ←─ 加密通信 ──→     │  ←─ 加密通信 ──→       │</span><br><span class="line">  │  (MITM可解密)        │  (正常HTTPS)           │</span><br></pre></td></tr></table></figure><p>核心原理：</p><ol><li><strong>中间人向客户端提供伪造证书</strong>，同时向服务端发起正常的 HTTPS 请求</li><li>客户端需要<strong>信任中间人的 CA 证书</strong>（这是关键前提）</li><li>中间人分别与客户端和服务端建立两段独立的加密连接</li><li>中间人可以解密客户端发来的数据，查看&#x2F;修改后，再加密发给服务端</li></ol><hr><h2 id="四、抓包原理与过程"><a href="#四、抓包原理与过程" class="headerlink" title="四、抓包原理与过程"></a>四、抓包原理与过程</h2><p>抓包（Packet Capture）本质上也是一种”中间人”行为，常见的抓包工具有 <strong>Fiddler</strong>、<strong>Charles</strong>、<strong>mitmproxy</strong> 等。</p><h3 id="4-1-抓包工具的工作原理"><a href="#4-1-抓包工具的工作原理" class="headerlink" title="4.1 抓包工具的工作原理"></a>4.1 抓包工具的工作原理</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></pre></td><td class="code"><pre><span class="line">┌──────────┐       ┌──────────────┐       ┌──────────┐</span><br><span class="line">│          │       │              │       │          │</span><br><span class="line">│  浏览器   │──────►│   代理服务器   │──────►│  目标服务器 │</span><br><span class="line">│ (Browser) │ HTTP  │  (Proxy)     │ HTTP  │ (Server) │</span><br><span class="line">│          │◄──────│  e.g. Fiddler │◄──────│          │</span><br><span class="line">└──────────┘       └──────────────┘       └──────────┘</span><br><span class="line">     │                    │</span><br><span class="line">     │                    │</span><br><span class="line">     ▼                    ▼</span><br><span class="line">  设置代理            抓包 &amp; 展示</span><br><span class="line">  127.0.0.1:8888     请求/响应详情</span><br></pre></td></tr></table></figure><p>抓包工具的核心步骤：</p><table><thead><tr><th>步骤</th><th>说明</th></tr></thead><tbody><tr><td><strong>1. 设置代理</strong></td><td>客户端（浏览器&#x2F;App）将 HTTP&#x2F;HTTPS 请求发送到代理服务器</td></tr><tr><td><strong>2. 转发请求</strong></td><td>代理服务器将请求转发给真正的目标服务器</td></tr><tr><td><strong>3. 接收响应</strong></td><td>目标服务器返回响应给代理服务器</td></tr><tr><td><strong>4. 返回客户端</strong></td><td>代理服务器将响应返回给客户端</td></tr><tr><td><strong>5. 记录展示</strong></td><td>代理服务器在中间记录所有请求和响应数据</td></tr></tbody></table><h3 id="4-2-HTTP-抓包（明文）"><a href="#4-2-HTTP-抓包（明文）" class="headerlink" title="4.2 HTTP 抓包（明文）"></a>4.2 HTTP 抓包（明文）</h3><p>对于 HTTP 请求，数据本身就是明文传输，抓包工具只需要简单地转发即可看到所有内容：</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><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">浏览器                         Fiddler                        服务器</span><br><span class="line">  │                              │                              │</span><br><span class="line">  │  GET /api/user HTTP/1.1      │                              │</span><br><span class="line">  │  Cookie: token=abc123        │                              │</span><br><span class="line">  │ ────────────────────────────►│  GET /api/user HTTP/1.1      │</span><br><span class="line">  │                              │  Cookie: token=abc123        │</span><br><span class="line">  │                              │ ────────────────────────────►│</span><br><span class="line">  │                              │                              │</span><br><span class="line">  │                              │  HTTP/1.1 200 OK             │</span><br><span class="line">  │                              │  &#123;&quot;name&quot;:&quot;张三&quot;,&quot;age&quot;:25&#125;     │</span><br><span class="line">  │                              │ ◄────────────────────────────│</span><br><span class="line">  │  HTTP/1.1 200 OK             │                              │</span><br><span class="line">  │  &#123;&quot;name&quot;:&quot;张三&quot;,&quot;age&quot;:25&#125;     │                              │</span><br><span class="line">  │ ◄─────────────────────────────│                              │</span><br><span class="line">  │                              │                              │</span><br><span class="line">  │                              │  ✅ 完整记录请求和响应          │</span><br></pre></td></tr></table></figure><p>HTTP 抓包无需任何额外配置，因为数据本身没有加密。</p><h3 id="4-3-HTTPS-抂包（加密流量）"><a href="#4-3-HTTPS-抂包（加密流量）" class="headerlink" title="4.3 HTTPS 抂包（加密流量）"></a>4.3 HTTPS 抂包（加密流量）</h3><p>HTTPS 流量是加密的，抓包工具需要通过中间人方式解密：</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><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">                  HTTPS 抓包完整流程</span><br><span class="line"></span><br><span class="line">浏览器                          Fiddler                       服务器</span><br><span class="line">  │                               │                              │</span><br><span class="line">  │  ① 设置系统代理                 │                              │</span><br><span class="line">  │  → 127.0.0.1:8888             │                              │</span><br><span class="line">  │                               │                              │</span><br><span class="line">  │  ② 安装并信任 Fiddler 根证书    │                              │</span><br><span class="line">  │  (FiddlerRoot Certificate)     │                              │</span><br><span class="line">  │                               │                              │</span><br><span class="line">  │  ③ HTTPS 握手                  │                              │</span><br><span class="line">  │  ClientHello ─────────────────►│                              │</span><br><span class="line">  │                               │  ④ 与服务器建立真实 HTTPS      │</span><br><span class="line">  │                               │  ClientHello ────────────────►│</span><br><span class="line">  │                               │  ServerHello+真证书 ◄─────────│</span><br><span class="line">  │                               │                              │</span><br><span class="line">  │  ⑤ 向浏览器出示伪造证书          │                              │</span><br><span class="line">  │  ServerHello+伪造证书 ◄────────│                              │</span><br><span class="line">  │                               │                              │</span><br><span class="line">  │  ⑥ 浏览器验证伪造证书 ✅         │                              │</span><br><span class="line">  │  (因为已信任 Fiddler 根证书)     │                              │</span><br><span class="line">  │                               │                              │</span><br><span class="line">  │  ⑦ 浏览器用伪造公钥加密数据 ────►│  ⑧ Fiddler 解密数据(明文)     │</span><br><span class="line">  │                               │  ⑨ Fiddler 用真公钥加密 ──────►│</span><br><span class="line">  │                               │                              │</span><br><span class="line">  │                               │  ⑩ 服务器返回加密响应 ◄────────│</span><br><span class="line">  │                               │  ⑪ Fiddler 解密(明文)         │</span><br><span class="line">  │  ⑫ Fiddler 加密返回浏览器 ◄────│                              │</span><br><span class="line">  │                               │                              │</span><br><span class="line">  │                               │  ✅ 中间明文查看所有数据        │</span><br></pre></td></tr></table></figure><h3 id="4-4-抓包工具实操步骤（以-Fiddler-为例）"><a href="#4-4-抓包工具实操步骤（以-Fiddler-为例）" class="headerlink" title="4.4 抓包工具实操步骤（以 Fiddler 为例）"></a>4.4 抓包工具实操步骤（以 Fiddler 为例）</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></pre></td><td class="code"><pre><span class="line">步骤流程图：</span><br><span class="line"></span><br><span class="line">  ┌─────────────────────┐</span><br><span class="line">  │ 1. 启动 Fiddler      │</span><br><span class="line">  │    默认监听 8888 端口  │</span><br><span class="line">  └──────────┬──────────┘</span><br><span class="line">             │</span><br><span class="line">             ▼</span><br><span class="line">  ┌─────────────────────┐</span><br><span class="line">  │ 2. 开启 HTTPS 解密   │</span><br><span class="line">  │    Tools → Options   │</span><br><span class="line">  │    → HTTPS → Decrypt │</span><br><span class="line">  └──────────┬──────────┘</span><br><span class="line">             │</span><br><span class="line">             ▼</span><br><span class="line">  ┌─────────────────────┐</span><br><span class="line">  │ 3. 安装根证书到系统   │</span><br><span class="line">  │    浏览器/系统信任     │</span><br><span class="line">  │    FiddlerRoot CA    │</span><br><span class="line">  └──────────┬──────────┘</span><br><span class="line">             │</span><br><span class="line">             ▼</span><br><span class="line">  ┌─────────────────────┐</span><br><span class="line">  │ 4. 配置客户端代理     │</span><br><span class="line">  │    127.0.0.1:8888    │</span><br><span class="line">  └──────────┬──────────┘</span><br><span class="line">             │</span><br><span class="line">             ▼</span><br><span class="line">  ┌─────────────────────┐</span><br><span class="line">  │ 5. 正常操作应用       │</span><br><span class="line">  │    Fiddler 自动抓包   │</span><br><span class="line">  └──────────┬──────────┘</span><br><span class="line">             │</span><br><span class="line">             ▼</span><br><span class="line">  ┌─────────────────────┐</span><br><span class="line">  │ 6. 查看请求/响应详情  │</span><br><span class="line">  │    Headers、Body、    │</span><br><span class="line">  │    Cookies 等        │</span><br><span class="line">  └─────────────────────┘</span><br></pre></td></tr></table></figure><hr><h2 id="五、常见中间人攻击手段"><a href="#五、常见中间人攻击手段" class="headerlink" title="五、常见中间人攻击手段"></a>五、常见中间人攻击手段</h2><h3 id="5-1-ARP-欺骗"><a href="#5-1-ARP-欺骗" class="headerlink" title="5.1 ARP 欺骗"></a>5.1 ARP 欺骗</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></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><span class="line">       │                      │</span><br><span class="line">       ▼                      ▼</span><br><span class="line">┌──────────┐          ┌──────────┐</span><br><span class="line">│  受害者   │          │   网关    │</span><br><span class="line">│  192.168  │          │ 192.168  │</span><br><span class="line">│  .1.100  │          │  .1.1    │</span><br><span class="line">└──────────┘          └──────────┘</span><br><span class="line">       ▲</span><br><span class="line">       │ ARP 欺骗</span><br><span class="line">       │</span><br><span class="line">┌──────────┐</span><br><span class="line">│  攻击者   │</span><br><span class="line">│  192.168  │</span><br><span class="line">│  .1.200  │</span><br><span class="line">└──────────┘</span><br></pre></td></tr></table></figure><p>ARP 欺骗原理：</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></pre></td><td class="code"><pre><span class="line">正常 ARP 表：                    被欺骗后的 ARP 表：</span><br><span class="line">┌───────────────────────┐       ┌───────────────────────┐</span><br><span class="line">│ IP           MAC      │       │ IP           MAC      │</span><br><span class="line">├───────────────────────┤       ├───────────────────────┤</span><br><span class="line">│ 192.168.1.1  AA-BB-CC │       │ 192.168.1.1  DD-EE-FF │ ← 被篡改</span><br><span class="line">│ 192.168.1.200 DD-EE-FF│       │ 192.168.1.200 DD-EE-FF│</span><br><span class="line">└───────────────────────┘       └───────────────────────┘</span><br><span class="line">(网关的 MAC 是路由器的)          (网关的 MAC 变成了攻击者的)</span><br></pre></td></tr></table></figure><p>攻击者告诉受害者”我是网关”，受害者的数据就会发给攻击者。</p><h3 id="5-2-DNS-欺骗"><a href="#5-2-DNS-欺骗" class="headerlink" title="5.2 DNS 欺骗"></a>5.2 DNS 欺骗</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></pre></td><td class="code"><pre><span class="line">正常 DNS 解析：                     DNS 欺骗后：</span><br><span class="line"></span><br><span class="line">  用户 → DNS服务器                    用户 → DNS服务器</span><br><span class="line">         (询问: bank.com 的IP?)              (询问: bank.com 的IP?)</span><br><span class="line">  用户 ← DNS服务器                    用户 ← 攻击者(伪造响应)</span><br><span class="line">         (回答: 93.184.216.34)               (回答: 6.6.6.6 ← 恶意IP)</span><br><span class="line">  用户 → 93.184.216.34               用户 → 6.6.6.6</span><br><span class="line">         (真正的银行服务器)                     (攻击者的钓鱼网站)</span><br></pre></td></tr></table></figure><h3 id="5-3-Wi-Fi-热点欺骗"><a href="#5-3-Wi-Fi-热点欺骗" class="headerlink" title="5.3 Wi-Fi 热点欺骗"></a>5.3 Wi-Fi 热点欺骗</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></pre></td><td class="code"><pre><span class="line">┌──────────────────────────────────────────┐</span><br><span class="line">│            公共场所 Wi-Fi 场景             │</span><br><span class="line">│                                          │</span><br><span class="line">│   ┌──────────┐     ┌──────────┐          │</span><br><span class="line">│   │  用户 A   │     │  用户 B   │          │</span><br><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><span class="line">│   │  &quot;Free_WiFi&quot; (攻击者热点)   │         │</span><br><span class="line">│   │  📡 恶意 AP               │         │</span><br><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><span class="line">│   └──────────────────┘                   │</span><br><span class="line">└──────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><p>攻击者创建一个与合法 Wi-Fi 同名或诱人的免费热点，用户连接后所有流量都经过攻击者。</p><h3 id="5-4-SSL-剥离（SSL-Stripping）"><a href="#5-4-SSL-剥离（SSL-Stripping）" class="headerlink" title="5.4 SSL 剥离（SSL Stripping）"></a>5.4 SSL 剥离（SSL Stripping）</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></pre></td><td class="code"><pre><span class="line">正常流程：                          SSL 剥离攻击：</span><br><span class="line"></span><br><span class="line">用户输入 bank.com                   用户输入 bank.com</span><br><span class="line">     │                                   │</span><br><span class="line">     ▼                                   ▼</span><br><span class="line">浏览器 → http://bank.com            浏览器 → http://bank.com</span><br><span class="line">     │                                   │</span><br><span class="line">     ▼                                   ▼</span><br><span class="line">服务器 301 重定向                     攻击者拦截重定向!</span><br><span class="line">→ https://bank.com                       │</span><br><span class="line">     │                                   ▼</span><br><span class="line">     ▼                              不转发重定向</span><br><span class="line">建立 HTTPS 安全连接                  保持 HTTP 明文连接</span><br><span class="line">     │                                   │</span><br><span class="line">     ▼                                   ▼</span><br><span class="line">✅ 安全通信                          ❌ 攻击者可查看所有数据</span><br><span class="line"></span><br><span class="line">用户看到的是 http:// 而不是 https://</span><br><span class="line">但大多数用户不会注意到这个区别</span><br></pre></td></tr></table></figure><hr><h2 id="六、如何防御中间人攻击"><a href="#六、如何防御中间人攻击" class="headerlink" title="六、如何防御中间人攻击"></a>六、如何防御中间人攻击</h2><table><thead><tr><th>防御手段</th><th>说明</th></tr></thead><tbody><tr><td><strong>始终使用 HTTPS</strong></td><td>确保网站强制使用 HTTPS，启用 HSTS</td></tr><tr><td><strong>证书锁定（Certificate Pinning）</strong></td><td>App 内置证书指纹，拒绝其他证书</td></tr><tr><td><strong>验证证书信息</strong></td><td>检查证书颁发者、有效期、域名是否匹配</td></tr><tr><td><strong>不要连接不信任的 Wi-Fi</strong></td><td>避免使用公共 Wi-Fi 处理敏感信息</td></tr><tr><td><strong>使用 VPN</strong></td><td>加密所有网络流量，防止窃听</td></tr><tr><td><strong>DNS over HTTPS (DoH)</strong></td><td>防止 DNS 查询被篡改</td></tr><tr><td><strong>双向 TLS 认证（mTLS）</strong></td><td>客户端也验证服务端，服务端也验证客户端</td></tr></tbody></table><hr><h2 id="七、总结"><a href="#七、总结" class="headerlink" title="七、总结"></a>七、总结</h2><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></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">│   ├── 网络层面：ARP/DNS/Wi-Fi 欺骗                   │</span><br><span class="line">│   ├── 协议层面：SSL 剥离、降级攻击                     │</span><br><span class="line">│   └── 证书层面：伪造 CA、自签名证书                    │</span><br><span class="line">│                                                     │</span><br><span class="line">│   攻击效果：                                         │</span><br><span class="line">│   ├── 窃听：获取敏感信息（密码、Cookie、Token）         │</span><br><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><span class="line">│   ├── 代理工具分别与双方建立加密连接                    │</span><br><span class="line">│   └── 在中间以明文形式查看和记录数据                    │</span><br><span class="line">└─────────────────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><blockquote><p><strong>核心认知</strong>：抓包工具（Fiddler&#x2F;Charles&#x2F;mitmproxy）的原理与恶意中间人攻击完全相同，区别仅在于<strong>目的不同</strong>——一个用于开发调试，一个用于恶意窃取。理解中间人攻击的原理，是掌握网络安全的基石。</p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;中间人攻击（MITM）原理与抓包分析&quot;&gt;&lt;a href=&quot;#中间人攻击（MITM）原理与抓包分析&quot; class=&quot;headerlink&quot; title=&quot;中间人攻击（MITM）原理与抓包分析&quot;&gt;&lt;/a&gt;中间人攻击（MITM）原理与抓包分析&lt;/h1&gt;&lt;h2 id=&quot;一</summary>
      
    
    
    
    <category term="前端" scheme="http://blog.ypintao.com/categories/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="逆向" scheme="http://blog.ypintao.com/categories/%E5%89%8D%E7%AB%AF/%E9%80%86%E5%90%91/"/>
    
    
    <category term="逆向" scheme="http://blog.ypintao.com/tags/%E9%80%86%E5%90%91/"/>
    
    <category term="安全" scheme="http://blog.ypintao.com/tags/%E5%AE%89%E5%85%A8/"/>
    
    <category term="网络安全" scheme="http://blog.ypintao.com/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    
  </entry>
  
  <entry>
    <title></title>
    <link href="http://blog.ypintao.com/2026/05/14/%E5%89%8D%E7%AB%AF/AI/task/"/>
    <id>http://blog.ypintao.com/2026/05/14/%E5%89%8D%E7%AB%AF/AI/task/</id>
    <published>2026-05-14T15:23:58.361Z</published>
    <updated>2026-05-20T14:30:37.423Z</updated>
    
    <content type="html"><![CDATA[<p>超级IP智能体<br>提取抖音文案，文案改写，语音克隆<br>视频对口型</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;超级IP智能体&lt;br&gt;提取抖音文案，文案改写，语音克隆&lt;br&gt;视频对口型&lt;/p&gt;
</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>关于低代码DSL转vue3组件的实现</title>
    <link href="http://blog.ypintao.com/2026/05/10/%E5%89%8D%E7%AB%AF/%E4%BD%8E%E4%BB%A3%E7%A0%81/%E5%85%B3%E4%BA%8E%E4%BD%8E%E4%BB%A3%E7%A0%81DSL%E8%BD%ACvue3%E7%BB%84%E4%BB%B6%E7%9A%84%E5%AE%9E%E7%8E%B0/"/>
    <id>http://blog.ypintao.com/2026/05/10/%E5%89%8D%E7%AB%AF/%E4%BD%8E%E4%BB%A3%E7%A0%81/%E5%85%B3%E4%BA%8E%E4%BD%8E%E4%BB%A3%E7%A0%81DSL%E8%BD%ACvue3%E7%BB%84%E4%BB%B6%E7%9A%84%E5%AE%9E%E7%8E%B0/</id>
    <published>2026-05-09T16:00:00.000Z</published>
    <updated>2026-05-13T23:58:04.733Z</updated>
    
    <content type="html"><![CDATA[<span id="more"></span><h1 id="DSL-渲染为-Vue3-页面组件-—-完整流程"><a href="#DSL-渲染为-Vue3-页面组件-—-完整流程" class="headerlink" title="DSL 渲染为 Vue3 页面组件 — 完整流程"></a>DSL 渲染为 Vue3 页面组件 — 完整流程</h1><h2 id="整体流程概览"><a href="#整体流程概览" class="headerlink" title="整体流程概览"></a>整体流程概览</h2><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></pre></td><td class="code"><pre><span class="line">BlockSchema (JSON DSL)</span><br><span class="line">       │</span><br><span class="line">       ▼</span><br><span class="line">  createRenderer()</span><br><span class="line">       │</span><br><span class="line">       ├─ 1. 创建 Context 上下文对象</span><br><span class="line">       │</span><br><span class="line">       ├─ 2. 定义 Vue 组件 (defineComponent)</span><br><span class="line">       │     ├─ setup(): 初始化 state / computed / methods / watch</span><br><span class="line">       │     └─ render(): 递归调用 nodeRender() 生成 VNode 树</span><br><span class="line">       │</span><br><span class="line">       └─ 3. 返回 &#123; renderer, context &#125;</span><br><span class="line">              │</span><br><span class="line">              ▼</span><br><span class="line">         &lt;Suspense&gt;&lt;component :is=&quot;renderer&quot; /&gt;&lt;/Suspense&gt;</span><br><span class="line">              │</span><br><span class="line">              ▼</span><br><span class="line">         页面渲染完成</span><br></pre></td></tr></table></figure><hr><h2 id="Step-1-DSL-数据结构定义"><a href="#Step-1-DSL-数据结构定义" class="headerlink" title="Step 1: DSL 数据结构定义"></a>Step 1: DSL 数据结构定义</h2><p><strong>代码路径</strong>: <code>src/core/types.ts</code></p><p>DSL 使用 JSON 描述页面，核心类型：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 页面级 Schema</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">BlockSchema</span> &#123;</span><br><span class="line">  <span class="attr">id</span>?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;                          <span class="comment">// 页面名称</span></span><br><span class="line">  <span class="attr">state</span>?: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSExpression</span>&gt;;  <span class="comment">// 响应式状态</span></span><br><span class="line">  <span class="attr">computed</span>?: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSFunction</span>&gt;; <span class="comment">// 计算属性</span></span><br><span class="line">  <span class="attr">methods</span>?: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSFunction</span>&gt;;  <span class="comment">// 方法</span></span><br><span class="line">  <span class="attr">watch</span>?: <span class="built_in">any</span>[];                         <span class="comment">// 侦听器</span></span><br><span class="line">  <span class="attr">lifeCycles</span>?: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSFunction</span>&gt;; <span class="comment">// 生命周期</span></span><br><span class="line">  <span class="attr">nodes</span>?: <span class="title class_">NodeSchema</span>[];                  <span class="comment">// 子节点树</span></span><br><span class="line">  <span class="attr">css</span>?: <span class="built_in">string</span>;                          <span class="comment">// 样式</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 节点 Schema</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">NodeSchema</span> &#123;</span><br><span class="line">  <span class="attr">id</span>?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;              <span class="comment">// HTML 标签名或组件名 (如 &#x27;div&#x27;, &#x27;button&#x27;)</span></span><br><span class="line">  <span class="attr">props</span>?: <span class="title class_">NodeProps</span>;         <span class="comment">// 属性 (含 style, class)</span></span><br><span class="line">  <span class="attr">events</span>?: <span class="title class_">NodeEvents</span>;       <span class="comment">// 事件绑定</span></span><br><span class="line">  <span class="attr">directives</span>?: <span class="title class_">NodeDirective</span>[]; <span class="comment">// 指令 (v-if, v-for, v-model...)</span></span><br><span class="line">  <span class="attr">children</span>?: <span class="built_in">string</span> | <span class="title class_">JSExpression</span> | <span class="title class_">NodeSchema</span>[]; <span class="comment">// 子节点</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 动态表达式 — 在运行时通过 new Function 求值</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">JSExpression</span> &#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;JSExpression&#x27;</span>;</span><br><span class="line">  <span class="attr">value</span>: <span class="built_in">string</span>;  <span class="comment">// 如 &quot;count + 1&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 动态函数 — 在运行时通过 new Function 生成函数</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">JSFunction</span> &#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;JSFunction&#x27;</span>;</span><br><span class="line">  <span class="attr">value</span>: <span class="built_in">string</span>;  <span class="comment">// 如 &quot;function() &#123; return this.count + 1 &#125;&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="Step-2-创建渲染器-—-createRenderer"><a href="#Step-2-创建渲染器-—-createRenderer" class="headerlink" title="Step 2: 创建渲染器 — createRenderer()"></a>Step 2: 创建渲染器 — createRenderer()</h2><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code> → <code>createRenderer()</code></p><p><strong>重点方法</strong>: <code>createRenderer(options)</code></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">createRenderer</span>(<span class="params"><span class="attr">options</span>: <span class="title class_">CreateRendererOptions</span></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> V = <span class="title class_">Vue</span>;</span><br><span class="line">  <span class="keyword">const</span> ctx = <span class="keyword">new</span> <span class="title class_">Context</span>(&#123; <span class="attr">dsl</span>: options.<span class="property">dsl</span> &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 核心：定义一个 Vue 组件</span></span><br><span class="line">  <span class="keyword">const</span> renderer = V.<span class="title function_">defineComponent</span>(&#123;</span><br><span class="line">    <span class="attr">name</span>: dsl.<span class="property">name</span> || <span class="string">&#x27;DslRenderer&#x27;</span>,</span><br><span class="line"></span><br><span class="line">    <span class="comment">// setup 是 async（所以外层需要 &lt;Suspense&gt;）</span></span><br><span class="line">    <span class="keyword">async</span> <span class="title function_">setup</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="comment">// 2a. 创建响应式 state</span></span><br><span class="line">      ctx.<span class="property">state</span> = <span class="title function_">createState</span>(V, dsl.<span class="property">state</span>, ctx);</span><br><span class="line">      <span class="comment">// 2b. 创建 computed</span></span><br><span class="line">      <span class="keyword">const</span> computed = <span class="title function_">createComputed</span>(V, dsl.<span class="property">computed</span>, ctx);</span><br><span class="line">      <span class="comment">// 2c. 创建 methods</span></span><br><span class="line">      <span class="keyword">const</span> methods = <span class="title function_">createMethods</span>(dsl.<span class="property">methods</span>, ctx);</span><br><span class="line">      <span class="comment">// 2d. 绑定 Vue 实例到 Context</span></span><br><span class="line">      ctx.<span class="title function_">setup</span>(&#123; ...computed, ...methods &#125;, V);</span><br><span class="line">      <span class="comment">// 2e. 设置 watch</span></span><br><span class="line">      <span class="title function_">setWatches</span>(V, dsl.<span class="property">watch</span>, ctx);</span><br><span class="line">      <span class="comment">// 暴露给模板</span></span><br><span class="line">      <span class="keyword">return</span> &#123; <span class="attr">state</span>: ctx.<span class="property">state</span>, ...computed, ...methods &#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="title function_">createLifeCycles</span>(dsl.<span class="property">lifeCycles</span>, ctx),</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 渲染函数：递归遍历 nodes 生成 VNode</span></span><br><span class="line">    <span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> nodes = dsl.<span class="property">nodes</span> || [];</span><br><span class="line">      <span class="keyword">if</span> (nodes.<span class="property">length</span> === <span class="number">1</span>) <span class="keyword">return</span> <span class="title function_">nodeRender</span>(nodes[<span class="number">0</span>], ctx, V, nodes);</span><br><span class="line">      <span class="keyword">return</span> V.<span class="title function_">createVNode</span>(<span class="string">&#x27;div&#x27;</span>, &#123;&#125;,</span><br><span class="line">        nodes.<span class="title function_">map</span>(<span class="function"><span class="params">child</span> =&gt;</span> <span class="title function_">nodeRender</span>(child, ctx, V, nodes))</span><br><span class="line">      );</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 class="attr">renderer</span>: V.<span class="title function_">markRaw</span>(renderer), <span class="attr">context</span>: ctx &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>调用方式</strong> (在 CanvasApp.vue &#x2F; PreviewView.vue 中):</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> result = <span class="title function_">createRenderer</span>(&#123; <span class="attr">dsl</span>: blockSchema &#125;);</span><br><span class="line">renderer.<span class="property">value</span> = result.<span class="property">renderer</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 模板中使用（必须包 Suspense，因为 setup 是 async）</span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;<span class="name">component</span> <span class="attr">:is</span>=<span class="string">&quot;renderer&quot;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br></pre></td></tr></table></figure><hr><h2 id="Step-3-运行时上下文-—-Context-类"><a href="#Step-3-运行时上下文-—-Context-类" class="headerlink" title="Step 3: 运行时上下文 — Context 类"></a>Step 3: 运行时上下文 — Context 类</h2><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code> → <code>class Context</code></p><p>Context 是 DSL 表达式执行的运行环境，代理了 Vue 实例的所有能力：</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><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Context</span><br><span class="line">├── state        → Vue reactive 状态</span><br><span class="line">├── props        → 组件 props</span><br><span class="line">├── $refs        → 模板引用</span><br><span class="line">├── $emit        → 事件触发</span><br><span class="line">├── $slots       → 插槽</span><br><span class="line">├── $watch       → 侦听器</span><br><span class="line">├── __parseExpression()  → 执行 JSExpression</span><br><span class="line">├── __parseFunction()    → 执行 JSFunction</span><br><span class="line">├── __clone()            → 克隆上下文（用于 v-for 循环变量）</span><br><span class="line">└── __ref()              → 处理模板 ref 引用</span><br></pre></td></tr></table></figure><p><strong>重点方法</strong>:</p><ul><li><code>ctx.setup(attrs, V)</code> — 在 Vue setup() 中调用，绑定 getCurrentInstance() 到 ctx</li><li><code>ctx.__parseExpression(code)</code> — 将 JSExpression 字符串转为实际值</li><li><code>ctx.__parseFunction(code)</code> — 将 JSFunction 字符串转为可执行函数</li><li><code>ctx.__clone(context)</code> — 创建子上下文（v-for 中每个迭代项需要独立上下文）</li></ul><hr><h2 id="Step-4-表达式解析-—-parseExpression-parseFunction"><a href="#Step-4-表达式解析-—-parseExpression-parseFunction" class="headerlink" title="Step 4: 表达式解析 — parseExpression &#x2F; parseFunction"></a>Step 4: 表达式解析 — parseExpression &#x2F; parseFunction</h2><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code> → <code>parseExpression()</code>, <code>parseFunction()</code></p><p><strong>重点方法</strong>: <code>parseExpression(str, self)</code></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">parseExpression</span>(<span class="params"><span class="attr">str</span>: <span class="title class_">JSExpression</span>, <span class="attr">self</span>: <span class="built_in">any</span></span>) &#123;</span><br><span class="line">  <span class="comment">// 1. 替换 this 为 __self</span></span><br><span class="line">  <span class="keyword">let</span> tarStr = str.<span class="property">value</span>.<span class="title function_">replace</span>(<span class="regexp">/this(\W|$)/g</span>, <span class="string">`__self$1`</span>);</span><br><span class="line">  <span class="comment">// 2. 用 with 语句构建作用域</span></span><br><span class="line">  <span class="keyword">const</span> code = <span class="string">`with($scope || &#123;&#125;) &#123; &quot;use strict&quot;; var __self = arguments[0]; return <span class="subst">$&#123;tarStr&#125;</span> &#125;`</span>;</span><br><span class="line">  <span class="comment">// 3. new Function 动态执行</span></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Function</span>(<span class="string">&#x27;$scope&#x27;</span>, code)(self);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>原理</strong>: 利用 <code>new Function</code> + <code>with</code> 语句，让 DSL 中的表达式字符串可以访问 Context 上的 state&#x2F;props&#x2F;methods 等变量。</p><p><strong>示例</strong>:</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></pre></td><td class="code"><pre><span class="line">JSExpression: &#123; type: &#x27;JSExpression&#x27;, value: &#x27;count + 1&#x27; &#125;</span><br><span class="line">                    ↓ parseExpression</span><br><span class="line">结果: state.count + 1 的实际值</span><br></pre></td></tr></table></figure><hr><h2 id="Step-5-节点渲染-—-nodeRender-递归"><a href="#Step-5-节点渲染-—-nodeRender-递归" class="headerlink" title="Step 5: 节点渲染 — nodeRender() 递归"></a>Step 5: 节点渲染 — nodeRender() 递归</h2><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code> → <code>nodeRender()</code></p><p><strong>重点方法</strong>: <code>nodeRender(dsl, ctx, V, brothers, isBranch, index)</code></p><p>这是整个渲染器最核心的函数，将一个 NodeSchema 递归转为 Vue VNode：</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><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></pre></td><td class="code"><pre><span class="line">nodeRender(dsl, ctx)</span><br><span class="line">  │</span><br><span class="line">  ├─ 1. 处理指令 (v-if / v-else-if / v-else)</span><br><span class="line">  │     条件不满足 → 查找兄弟节点的 v-else-if / v-else</span><br><span class="line">  │</span><br><span class="line">  ├─ 2. 内部 render(c, seq) 函数:</span><br><span class="line">  │     ├─ parseNodeProps()   → 解析属性（递归处理 JSExpression）</span><br><span class="line">  │     ├─ parseNodeEvents()  → 解析事件（JSFunction → onXxx）</span><br><span class="line">  │     ├─ 处理 v-bind       → 合并动态属性</span><br><span class="line">  │     ├─ 处理 v-show        → 控制 display: none</span><br><span class="line">  │     ├─ 处理 v-html        → 设置 innerHTML</span><br><span class="line">  │     ├─ 处理 v-model       → 双向绑定（区分原生/组件）</span><br><span class="line">  │     ├─ childrenToSlots()  → 子节点转为插槽</span><br><span class="line">  │     └─ Vue.createVNode()  → 创建 VNode</span><br><span class="line">  │</span><br><span class="line">  ├─ 3. 处理 v-for → vForRender() 循环渲染</span><br><span class="line">  │     每次迭代 clone Context，注入 item/index 变量</span><br><span class="line">  │</span><br><span class="line">  └─ 4. 返回 VNode 或 VNode 数组</span><br></pre></td></tr></table></figure><hr><h2 id="Step-6-辅助函数详解"><a href="#Step-6-辅助函数详解" class="headerlink" title="Step 6: 辅助函数详解"></a>Step 6: 辅助函数详解</h2><h3 id="6-1-属性解析-—-deepParseNodeProps"><a href="#6-1-属性解析-—-deepParseNodeProps" class="headerlink" title="6.1 属性解析 — deepParseNodeProps()"></a>6.1 属性解析 — deepParseNodeProps()</h3><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code> → <code>deepParseNodeProps()</code></p><p>递归遍历 props 对象，将所有 JSExpression&#x2F;JSFunction 替换为实际值：</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></pre></td><td class="code"><pre><span class="line">props: &#123; class: &#123; type: &#x27;JSExpression&#x27;, value: &#x27;active ? &quot;active&quot; : &quot;&quot;&#x27; &#125; &#125;</span><br><span class="line">  ↓ deepParseNodeProps</span><br><span class="line">props: &#123; class: &quot;active&quot; &#125;  // 运行时求值结果</span><br></pre></td></tr></table></figure><h3 id="6-2-事件解析-—-parseNodeEvents"><a href="#6-2-事件解析-—-parseNodeEvents" class="headerlink" title="6.2 事件解析 — parseNodeEvents()"></a>6.2 事件解析 — parseNodeEvents()</h3><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code> → <code>parseNodeEvents()</code></p><p>将 DSL 事件定义转为 Vue 的 <code>onXxx</code> 格式：</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></pre></td><td class="code"><pre><span class="line">events: &#123; click: &#123; handler: &#123; type: &#x27;JSFunction&#x27;, value: &#x27;function() &#123; ... &#125;&#x27; &#125; &#125; &#125;</span><br><span class="line">  ↓ parseNodeEvents</span><br><span class="line">&#123; onClick: [Function] &#125;</span><br></pre></td></tr></table></figure><h3 id="6-3-指令处理-—-getDiretives"><a href="#6-3-指令处理-—-getDiretives" class="headerlink" title="6.3 指令处理 — getDiretives()"></a>6.3 指令处理 — getDiretives()</h3><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code> → <code>getDiretives()</code></p><p>将 directives 数组分类为 vIf&#x2F;vFor&#x2F;vShow&#x2F;vModel 等，方便后续分别处理。</p><h3 id="6-4-v-for-渲染-—-vForRender"><a href="#6-4-v-for-渲染-—-vForRender" class="headerlink" title="6.4 v-for 渲染 — vForRender()"></a>6.4 v-for 渲染 — vForRender()</h3><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code> → <code>vForRender()</code></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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 示例 DSL:</span></span><br><span class="line">&#123; <span class="attr">directives</span>: [&#123; <span class="attr">name</span>: <span class="string">&#x27;vFor&#x27;</span>, <span class="attr">value</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;JSExpression&#x27;</span>, <span class="attr">value</span>: <span class="string">&#x27;items&#x27;</span> &#125;, <span class="attr">iterator</span>: &#123; <span class="attr">item</span>: <span class="string">&#x27;item&#x27;</span>, <span class="attr">index</span>: <span class="string">&#x27;idx&#x27;</span> &#125; &#125;] &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 执行流程:</span></span><br><span class="line"><span class="comment">// 1. ctx.__parseExpression(value) → 获取数组 [1, 2, 3]</span></span><br><span class="line"><span class="comment">// 2. 遍历数组，每次 ctx.__clone(&#123; item: val, idx: i &#125;) 创建子上下文</span></span><br><span class="line"><span class="comment">// 3. 对每个子上下文调用 render(c, seq) 生成 VNode</span></span><br><span class="line"><span class="comment">// 4. 返回 VNode 数组</span></span><br></pre></td></tr></table></figure><h3 id="6-5-子节点转插槽-—-childrenToSlots"><a href="#6-5-子节点转插槽-—-childrenToSlots" class="headerlink" title="6.5 子节点转插槽 — childrenToSlots()"></a>6.5 子节点转插槽 — childrenToSlots()</h3><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code> → <code>childrenToSlots()</code>, <code>createSlotsConfig()</code></p><p>将 NodeSchema 的 children 转为 Vue 插槽对象：</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">children: [</span><br><span class="line">  &#123; name: &#x27;div&#x27;, slot: &#x27;header&#x27;, children: &#x27;标题&#x27; &#125;,</span><br><span class="line">  &#123; name: &#x27;div&#x27;, slot: &#x27;default&#x27;, children: &#x27;内容&#x27; &#125;</span><br><span class="line">]</span><br><span class="line">  ↓ childrenToSlots</span><br><span class="line">&#123;</span><br><span class="line">  header: () =&gt; [VNode],</span><br><span class="line">  default: () =&gt; [VNode]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-6-状态初始化-—-createState-createComputed-createMethods"><a href="#6-6-状态初始化-—-createState-createComputed-createMethods" class="headerlink" title="6.6 状态初始化 — createState() &#x2F; createComputed() &#x2F; createMethods()"></a>6.6 状态初始化 — createState() &#x2F; createComputed() &#x2F; createMethods()</h3><p><strong>代码路径</strong>: <code>src/core/renderer.ts</code></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></pre></td><td class="code"><pre><span class="line">BlockSchema.state    → createState()    → Vue.reactive(&#123; ... &#125;)</span><br><span class="line">BlockSchema.computed → createComputed() → &#123; key: Vue.computed(fn) &#125;</span><br><span class="line">BlockSchema.methods  → createMethods()  → &#123; key: Function &#125;</span><br><span class="line">BlockSchema.watch    → setWatches()     → Vue.watch(source, handler)</span><br></pre></td></tr></table></figure><hr><h2 id="Step-7-设计器中的渲染集成"><a href="#Step-7-设计器中的渲染集成" class="headerlink" title="Step 7: 设计器中的渲染集成"></a>Step 7: 设计器中的渲染集成</h2><h3 id="7-1-iframe-画布渲染"><a href="#7-1-iframe-画布渲染" class="headerlink" title="7.1 iframe 画布渲染"></a>7.1 iframe 画布渲染</h3><p><strong>代码路径</strong>: <code>src/canvas/CanvasApp.vue</code></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">DesignerView (主页面)</span><br><span class="line">  │ postMessage(&#x27;init&#x27;, dsl)</span><br><span class="line">  ▼</span><br><span class="line">CanvasApp.vue (iframe 内)</span><br><span class="line">  │ 收到 init → renderDsl()</span><br><span class="line">  │   ├─ createRenderer(&#123; dsl &#125;)</span><br><span class="line">  │   └─ renderer.value = result.renderer</span><br><span class="line">  ▼</span><br><span class="line">&lt;Suspense&gt;&lt;component :is=&quot;renderer&quot; /&gt;&lt;/Suspense&gt;</span><br></pre></td></tr></table></figure><h3 id="7-2-预览页渲染"><a href="#7-2-预览页渲染" class="headerlink" title="7.2 预览页渲染"></a>7.2 预览页渲染</h3><p><strong>代码路径</strong>: <code>src/views/PreviewView.vue</code></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></pre></td><td class="code"><pre><span class="line">sessionStorage.getItem(&#x27;vtj_preview_dsl&#x27;)</span><br><span class="line">  │ JSON.parse</span><br><span class="line">  ▼</span><br><span class="line">createRenderer(&#123; dsl &#125;)</span><br><span class="line">  │</span><br><span class="line">  ▼</span><br><span class="line">&lt;Suspense&gt;&lt;component :is=&quot;renderer&quot; /&gt;&lt;/Suspense&gt;</span><br></pre></td></tr></table></figure><hr><h2 id="代码文件索引"><a href="#代码文件索引" class="headerlink" title="代码文件索引"></a>代码文件索引</h2><table><thead><tr><th>文件</th><th>说明</th><th>核心方法</th></tr></thead><tbody><tr><td><code>src/core/types.ts</code></td><td>DSL 类型定义</td><td><code>BlockSchema</code>, <code>NodeSchema</code>, <code>JSExpression</code>, <code>JSFunction</code></td></tr><tr><td><code>src/core/renderer.ts</code></td><td>渲染器核心</td><td><code>createRenderer()</code>, <code>nodeRender()</code>, <code>Context</code></td></tr><tr><td></td><td>表达式解析</td><td><code>parseExpression()</code>, <code>parseFunction()</code></td></tr><tr><td></td><td>属性处理</td><td><code>deepParseNodeProps()</code>, <code>parseNodeProps()</code>, <code>parseNodeEvents()</code></td></tr><tr><td></td><td>指令处理</td><td><code>getDiretives()</code>, <code>vForRender()</code></td></tr><tr><td></td><td>插槽处理</td><td><code>childrenToSlots()</code>, <code>createSlotsConfig()</code></td></tr><tr><td></td><td>状态初始化</td><td><code>createState()</code>, <code>createComputed()</code>, <code>createMethods()</code>, <code>setWatches()</code>, <code>createLifeCycles()</code></td></tr><tr><td><code>src/canvas/CanvasApp.vue</code></td><td>iframe 画布应用</td><td><code>renderDsl()</code>, <code>addNode()</code>, <code>onDocumentClick()</code></td></tr><tr><td><code>src/canvas/protocol.ts</code></td><td>postMessage 通信</td><td><code>sendMessage()</code>, <code>onMessage()</code></td></tr><tr><td><code>src/views/PreviewView.vue</code></td><td>预览页</td><td><code>createRenderer()</code> + <code>&lt;Suspense&gt;</code></td></tr><tr><td><code>src/views/DesignerView.vue</code></td><td>设计器主页面</td><td><code>sendToCanvas()</code>, <code>onMaskMouseUp()</code></td></tr><tr><td><code>src/engine/Engine.ts</code></td><td>设计器引擎</td><td>DSL CRUD, 撤销&#x2F;重做, 节点操作</td></tr><tr><td><code>src/materials/index.ts</code></td><td>物料定义</td><td><code>createNodeSchema()</code></td></tr><tr><td><code>src/coder/index.ts</code></td><td>代码生成</td><td><code>generateVueSFC()</code>, <code>generateJSON()</code></td></tr></tbody></table><hr><h2 id="完整数据流图"><a href="#完整数据流图" class="headerlink" title="完整数据流图"></a>完整数据流图</h2><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></pre></td><td class="code"><pre><span class="line">用户拖拽物料</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">DesignerView.onDragStart()  ──→ dragSchema = schema</span><br><span class="line">    │ (鼠标松开在画布遮罩上)</span><br><span class="line">    ▼</span><br><span class="line">DesignerView.onMaskMouseUp()</span><br><span class="line">    │ postMessage(&#x27;dropNode&#x27;, &#123; schema, x, y &#125;)</span><br><span class="line">    ▼</span><br><span class="line">CanvasApp 收到 dropNode</span><br><span class="line">    │ addNode(parentId, schema)</span><br><span class="line">    │   ├─ dsl.nodes.push(node)</span><br><span class="line">    │   ├─ renderDsl() → createRenderer(&#123; dsl &#125;)</span><br><span class="line">    │   │   ├─ new Context(&#123; dsl &#125;)</span><br><span class="line">    │   │   ├─ defineComponent(&#123;</span><br><span class="line">    │   │   │     setup: createState + createComputed + createMethods</span><br><span class="line">    │   │   │     render: nodes.map(n =&gt; nodeRender(n, ctx))</span><br><span class="line">    │   │   │       ├─ parseNodeProps()   解析属性</span><br><span class="line">    │   │   │       ├─ parseNodeEvents()  解析事件</span><br><span class="line">    │   │   │       ├─ getDiretives()     处理指令</span><br><span class="line">    │   │   │       ├─ childrenToSlots()  处理子节点</span><br><span class="line">    │   │   │       └─ Vue.createVNode()  创建 VNode</span><br><span class="line">    │   │   │   &#125;)</span><br><span class="line">    │   │   └─ markRaw(renderer)</span><br><span class="line">    │   └─ renderer.value = result.renderer</span><br><span class="line">    ▼</span><br><span class="line">&lt;Suspense&gt;&lt;component :is=&quot;renderer&quot; /&gt;&lt;/Suspense&gt;</span><br><span class="line">    │</span><br><span class="line">    ▼</span><br><span class="line">页面渲染出组件</span><br><span class="line">    │ postMessage(&#x27;updateDsl&#x27;, dsl)  ←── 同步回主页面</span><br><span class="line">    ▼</span><br><span class="line">DesignerView 收到 updateDsl</span><br><span class="line">    │ eng.setDSL(dsl)  ←── 更新引擎状态</span><br><span class="line">    ▼</span><br><span class="line">右侧 PropsPanel 显示选中节点配置</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;span id=&quot;more&quot;&gt;&lt;/span&gt;
&lt;h1 id=&quot;DSL-渲染为-Vue3-页面组件-—-完整流程&quot;&gt;&lt;a href=&quot;#DSL-渲染为-Vue3-页面组件-—-完整流程&quot; class=&quot;headerlink&quot; title=&quot;DSL 渲染为 Vue3 页面组件 — </summary>
      
    
    
    
    <category term="低代码" scheme="http://blog.ypintao.com/categories/%E4%BD%8E%E4%BB%A3%E7%A0%81/"/>
    
    
    <category term="低代码" scheme="http://blog.ypintao.com/tags/%E4%BD%8E%E4%BB%A3%E7%A0%81/"/>
    
  </entry>
  
  <entry>
    <title>关于低代码相关的理解</title>
    <link href="http://blog.ypintao.com/2026/05/09/%E5%89%8D%E7%AB%AF/%E4%BD%8E%E4%BB%A3%E7%A0%81/%E5%85%B3%E4%BA%8E%E4%BD%8E%E4%BB%A3%E7%A0%81/"/>
    <id>http://blog.ypintao.com/2026/05/09/%E5%89%8D%E7%AB%AF/%E4%BD%8E%E4%BB%A3%E7%A0%81/%E5%85%B3%E4%BA%8E%E4%BD%8E%E4%BB%A3%E7%A0%81/</id>
    <published>2026-05-08T16:00:00.000Z</published>
    <updated>2026-05-10T08:24:25.627Z</updated>
    
    <content type="html"><![CDATA[<h1 id="大纲"><a href="#大纲" class="headerlink" title="大纲"></a>大纲</h1><p>前端在开发活动过程中，前端开发流程老的方式就是设计出图&#x3D;&gt;前端开发页面&#x3D;&gt;对接接口调整页面交互逻辑&#x3D;&gt;测试完上线&#x3D;&gt;运营配置活动。<br>低代码重点是为了满足大部分日常活动的需求，运营可自主配置活动并上线，释放了相当一部分的开发人力。<br>不过，此时的方案仍然无法很好地服务大型直播活动场景，比如年度 S 级的直播活动，这类活动赛程多且持续时间长（可长达两周），页面和组件都包含多种状态，运营难以配置出大型直播活动的所有需求，故此阶段的大型活动仍然完全由开发编码实现，需要占用较多人力，但此类活动从数量上来看连 1% 都不到。鉴于我们已在日常活动中积累维护了相当多功能的低代码组件，如何复用已有的低代码组件来更快实现更多活动玩法就成了一个值得研究的问题。此外，大型活动从筹备到结束，时间长达数月，而密集开发阶段可能只占其中的一个月，密集开发结束后的长尾需求又该如何优化提效？需要探索前端在活动场景的低代码相关经验，以及最终稳定的开发模式。</p><span id="more"></span><h2 id="不同场景的开发方案"><a href="#不同场景的开发方案" class="headerlink" title="不同场景的开发方案"></a>不同场景的开发方案</h2><h3 id="配置类页面"><a href="#配置类页面" class="headerlink" title="配置类页面"></a>配置类页面</h3><p>经产运及技术负责人评估后，若无需开发参与则由产运同事自助配置并上线。若需要开发参与，则视情况修改组件或开发 JS 或 CSS 补丁代码来实现，沉淀下来的代码也可用于后续的类似活动。</p><h3 id="开发类页面"><a href="#开发类页面" class="headerlink" title="开发类页面"></a>开发类页面</h3><p>在一些玩法较为复杂、活动状态多的场景，配置页面的难度可能会呈指数级上升，当时平台也并不具备完善的预览能力，所以此类活动几乎完全由开发负责实现（部分页面可能会跳转到配置类页面）。虽然工作量较大，但简单来说就是根据设计稿、产品需求文档、接口文档这几个关键输入来实现活动页面，在流程上并没有太多可说的。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;大纲&quot;&gt;&lt;a href=&quot;#大纲&quot; class=&quot;headerlink&quot; title=&quot;大纲&quot;&gt;&lt;/a&gt;大纲&lt;/h1&gt;&lt;p&gt;前端在开发活动过程中，前端开发流程老的方式就是设计出图&amp;#x3D;&amp;gt;前端开发页面&amp;#x3D;&amp;gt;对接接口调整页面交互逻辑&amp;#x3D;&amp;gt;测试完上线&amp;#x3D;&amp;gt;运营配置活动。&lt;br&gt;低代码重点是为了满足大部分日常活动的需求，运营可自主配置活动并上线，释放了相当一部分的开发人力。&lt;br&gt;不过，此时的方案仍然无法很好地服务大型直播活动场景，比如年度 S 级的直播活动，这类活动赛程多且持续时间长（可长达两周），页面和组件都包含多种状态，运营难以配置出大型直播活动的所有需求，故此阶段的大型活动仍然完全由开发编码实现，需要占用较多人力，但此类活动从数量上来看连 1% 都不到。鉴于我们已在日常活动中积累维护了相当多功能的低代码组件，如何复用已有的低代码组件来更快实现更多活动玩法就成了一个值得研究的问题。此外，大型活动从筹备到结束，时间长达数月，而密集开发阶段可能只占其中的一个月，密集开发结束后的长尾需求又该如何优化提效？需要探索前端在活动场景的低代码相关经验，以及最终稳定的开发模式。&lt;/p&gt;</summary>
    
    
    
    <category term="低代码" scheme="http://blog.ypintao.com/categories/%E4%BD%8E%E4%BB%A3%E7%A0%81/"/>
    
    
    <category term="低代码" scheme="http://blog.ypintao.com/tags/%E4%BD%8E%E4%BB%A3%E7%A0%81/"/>
    
  </entry>
  
  <entry>
    <title>从DSL 到渲染出vue3组件的几种方式</title>
    <link href="http://blog.ypintao.com/2026/05/09/%E5%89%8D%E7%AB%AF/%E4%BD%8E%E4%BB%A3%E7%A0%81/%E4%BB%8EDSL%20%E5%88%B0%E6%B8%B2%E6%9F%93%E5%87%BAvue3%E7%BB%84%E4%BB%B6%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E5%BC%8F/"/>
    <id>http://blog.ypintao.com/2026/05/09/%E5%89%8D%E7%AB%AF/%E4%BD%8E%E4%BB%A3%E7%A0%81/%E4%BB%8EDSL%20%E5%88%B0%E6%B8%B2%E6%9F%93%E5%87%BAvue3%E7%BB%84%E4%BB%B6%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E5%BC%8F/</id>
    <published>2026-05-08T16:00:00.000Z</published>
    <updated>2026-05-14T00:07:27.740Z</updated>
    
    <content type="html"><![CDATA[<p>将 DSL（Domain-Specific Language，领域特定语言，通常在前端表现为 JSON 或 XML 格式的数据结构）渲染为 Vue 3 组件，是<strong>低代码（Low-Code）、动态表单、CMS 页面可视化搭建</strong>等场景的核心诉求。</p><p>在 Vue 3 中，实现从 DSL 到组件渲染主要有以下几种方式，按<strong>推荐程度和常用场景</strong>排序：</p><hr><h3 id="假设我们有如下的基础-DSL-结构："><a href="#假设我们有如下的基础-DSL-结构：" class="headerlink" title="假设我们有如下的基础 DSL 结构："></a>假设我们有如下的基础 DSL 结构：</h3><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;div&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;props&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="attr">&quot;class&quot;</span><span class="punctuation">:</span> <span class="string">&quot;container&quot;</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;children&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;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;MyButton&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;props&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;primary&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;text&quot;</span><span class="punctuation">:</span> <span class="string">&quot;点击我&quot;</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;children&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><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><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><hr><h3 id="1-递归动态组件（）"><a href="#1-递归动态组件（）" class="headerlink" title="1. 递归动态组件（&lt;component :is=&quot;xxx&quot;&gt;）"></a>1. 递归动态组件（<code>&lt;component :is=&quot;xxx&quot;&gt;</code>）</h3><p>这是<strong>最符合 Vue 模板开发直觉</strong>、使用最广泛的方式。通过编写一个递归的 Vue 组件，解析 DSL 的每一个节点。</p><ul><li><strong>实现原理</strong>：利用 Vue 的 <code>&lt;component :is=&quot;...&quot;&gt;</code> 动态挂载组件，并结合 <code>v-for</code> 和组件自身的递归调用来渲染子节点。</li><li><strong>代码示例</strong>：</li></ul><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></pre></td><td class="code"><pre><span class="line">&lt;!-- DslRenderer.vue --&gt;</span><br><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;!-- 动态渲染当前节点 --&gt;</span><br><span class="line">  &lt;component :is=&quot;resolveTag(dsl.type)&quot; v-bind=&quot;dsl.props&quot;&gt;</span><br><span class="line">    &lt;!-- 如果有文本内容 --&gt;</span><br><span class="line">    &lt;template v-if=&quot;dsl.props.text&quot;&gt;&#123;&#123; dsl.props.text &#125;&#125;&lt;/template&gt;</span><br><span class="line">    </span><br><span class="line">    &lt;!-- 递归渲染子节点 --&gt;</span><br><span class="line">    &lt;template v-if=&quot;dsl.children &amp;&amp; dsl.children.length&quot;&gt;</span><br><span class="line">      &lt;DslRenderer </span><br><span class="line">        v-for=&quot;(child, index) in dsl.children&quot; </span><br><span class="line">        :key=&quot;index&quot; </span><br><span class="line">        :dsl=&quot;child&quot; </span><br><span class="line">      /&gt;</span><br><span class="line">    &lt;/template&gt;</span><br><span class="line">  &lt;/component&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script setup&gt;</span><br><span class="line">import &#123; resolveComponent &#125; from &#x27;vue&#x27;;</span><br><span class="line">// 预先引入自定义组件</span><br><span class="line">import MyButton from &#x27;./MyButton.vue&#x27;;</span><br><span class="line"></span><br><span class="line">const props = defineProps(&#123;</span><br><span class="line">  dsl: Object</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">// 解析标签（区分原生 HTML 标签和 Vue 组件）</span><br><span class="line">const resolveTag = (type) =&gt; &#123;</span><br><span class="line">  const htmlTags = [&#x27;div&#x27;, &#x27;span&#x27;, &#x27;p&#x27;, &#x27;h1&#x27;, &#x27;section&#x27;];</span><br><span class="line">  if (htmlTags.includes(type)) &#123;</span><br><span class="line">    return type;</span><br><span class="line">  &#125;</span><br><span class="line">  // 如果全局注册过，也可以按需获取</span><br><span class="line">  const componentsMap = &#123; MyButton &#125;;</span><br><span class="line">  return componentsMap[type] || type;</span><br><span class="line">&#125;;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><ul><li><strong>优点</strong>：纯模板语法，心智负担小；天然支持 Vue 的指令（<code>v-if</code>, <code>v-show</code>）；易于集成现有的 UI 组件库。</li><li><strong>缺点</strong>：对于极其复杂的巨型 DOM 树，递归组件的实例开销较大。</li></ul><hr><h3 id="2-渲染函数（Render-Function-h-函数）"><a href="#2-渲染函数（Render-Function-h-函数）" class="headerlink" title="2. 渲染函数（Render Function &#x2F; h 函数）"></a>2. 渲染函数（Render Function &#x2F; <code>h</code> 函数）</h3><p>直接跳过模板编译阶段，使用 Vue 3 的底层 API <code>h()</code> 生成虚拟 DOM (VNode)。这是<strong>性能最好、最灵活</strong>的方式，也是大多数成熟开源低代码引擎（如阿里 LowCodeEngine 的 Vue 渲染器）的底层选择。</p><ul><li><strong>实现原理</strong>：编写一个递归的 JavaScript 函数，遍历 DSL 并不断调用 <code>h()</code> 生成 VNode 树。</li><li><strong>代码示例</strong>：</li></ul><figure class="highlight javascript"><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">// renderDsl.js</span></span><br><span class="line"><span class="keyword">import</span> &#123; h, resolveComponent, <span class="title class_">Text</span> &#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_">MyButton</span> <span class="keyword">from</span> <span class="string">&#x27;./MyButton.vue&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> componentsMap = &#123; <span class="title class_">MyButton</span> &#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_">renderDsl</span>(<span class="params">node</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (!node) <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 1. 获取组件类型</span></span><br><span class="line">  <span class="keyword">let</span> tag = node.<span class="property">type</span>;</span><br><span class="line">  <span class="comment">// 如果不是原生标签，则从映射表或全局获取</span></span><br><span class="line">  <span class="keyword">if</span> (![<span class="string">&#x27;div&#x27;</span>, <span class="string">&#x27;span&#x27;</span>, <span class="string">&#x27;p&#x27;</span>].<span class="title function_">includes</span>(tag)) &#123;</span><br><span class="line">    tag = componentsMap[tag] || <span class="title function_">resolveComponent</span>(tag);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 处理子节点</span></span><br><span class="line">  <span class="keyword">const</span> children = [];</span><br><span class="line">  <span class="keyword">if</span> (node.<span class="property">props</span>?.<span class="property">text</span>) &#123;</span><br><span class="line">    children.<span class="title function_">push</span>(node.<span class="property">props</span>.<span class="property">text</span>); <span class="comment">// 纯文本节点</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (node.<span class="property">children</span> &amp;&amp; node.<span class="property">children</span>.<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    children.<span class="title function_">push</span>(...node.<span class="property">children</span>.<span class="title function_">map</span>(<span class="function"><span class="params">child</span> =&gt;</span> <span class="title function_">renderDsl</span>(child)));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3. 返回 VNode</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">h</span>(tag, node.<span class="property">props</span>, children);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用时包装在一个函数式组件或普通组件的 <code>setup</code> 中：</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><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&lt;script&gt;</span><br><span class="line">import &#123; defineComponent &#125; from &#x27;vue&#x27;;</span><br><span class="line">import &#123; renderDsl &#125; from &#x27;./renderDsl&#x27;;</span><br><span class="line"></span><br><span class="line">export default defineComponent(&#123;</span><br><span class="line">  props: [&#x27;dsl&#x27;],</span><br><span class="line">  setup(props) &#123;</span><br><span class="line">    return () =&gt; renderDsl(props.dsl);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><ul><li><strong>优点</strong>：性能极高（没有模板解析开销，直接生成 VNode）；极度灵活，可以轻松实现复杂的逻辑流转（如动态插槽、事件拦截）。</li><li><strong>缺点</strong>：代码可读性稍差（尤其是多层嵌套时），需要开发者非常熟悉 Vue 的 VNode 设计规范。</li></ul><hr><h3 id="3-JSX-TSX-语法"><a href="#3-JSX-TSX-语法" class="headerlink" title="3. JSX &#x2F; TSX 语法"></a>3. JSX &#x2F; TSX 语法</h3><p>本质上是第 2 种方案（渲染函数）的语法糖。利用 Babel 将 JSX 编译成 <code>h()</code> 函数，使代码看起来更像 HTML 结构。</p><ul><li><strong>代码示例</strong>：</li></ul><figure class="highlight tsx"><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="comment">// DslRenderer.tsx</span></span><br><span class="line"><span class="keyword">import</span> &#123; defineComponent &#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_">MyButton</span> <span class="keyword">from</span> <span class="string">&#x27;./MyButton.vue&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">componentsMap</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt; = &#123; <span class="title class_">MyButton</span> &#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">DslRenderer</span> = <span class="title function_">defineComponent</span>(&#123;</span><br><span class="line">  <span class="attr">props</span>: &#123;</span><br><span class="line">    <span class="attr">dsl</span>: &#123; <span class="attr">type</span>: <span class="title class_">Object</span>, <span class="attr">required</span>: <span class="literal">true</span> &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="title function_">setup</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="title function_">renderNode</span> = (<span class="params"><span class="attr">node</span>: <span class="built_in">any</span></span>) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> <span class="title class_">Tag</span> = componentsMap[node.<span class="property">type</span>] || node.<span class="property">type</span>;</span><br><span class="line">      </span><br><span class="line">      <span class="keyword">return</span> (</span><br><span class="line">        <span class="language-xml"><span class="tag">&lt;<span class="name">Tag</span> &#123;<span class="attr">...node.props</span>&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;node.props.text &amp;&amp; node.props.text&#125;</span></span><br><span class="line"><span class="language-xml">          &#123;node.children?.map((child: any) =&gt; renderNode(child))&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">Tag</span>&gt;</span></span></span><br><span class="line">      );</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> <span class="title function_">renderNode</span>(props.<span class="property">dsl</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><ul><li><strong>优点</strong>：比手写 <code>h()</code> 可读性好太多，React 开发者可以零成本平滑迁移。</li><li><strong>缺点</strong>：需要配置构建工具（Vite&#x2F;Webpack）支持 JSX；部分 Vue 独有的模板指令（如 <code>v-model</code> 的修饰符、<code>v-slot</code>）在 JSX 中的写法较为反直觉。</li></ul><hr><h3 id="4-运行时模板编译（Runtime-Compiler）"><a href="#4-运行时模板编译（Runtime-Compiler）" class="headerlink" title="4. 运行时模板编译（Runtime Compiler）"></a>4. 运行时模板编译（Runtime Compiler）</h3><p>将 DSL 解析并<strong>拼接成一段 Vue template 字符串</strong>，然后利用 Vue 的运行时编译器将字符串动态编译为组件。</p><ul><li><strong>实现原理</strong>：DSL -&gt; Template String -&gt; <code>compile()</code> -&gt; Component</li><li><strong>代码示例</strong>：</li></ul><figure class="highlight javascript"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; defineComponent &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span><br><span class="line"><span class="comment">// 注意：必须使用带有编译器的 Vue 构建版本 (vue.esm-browser.js 或配置 alias)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 假设我们写了一个将 DSL 转为 HTML 字符串的函数</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">dslToTemplate</span>(<span class="params">dsl</span>) &#123;</span><br><span class="line">  <span class="comment">// 生成类似 &quot;&lt;div class=&#x27;container&#x27;&gt;&lt;MyButton type=&#x27;primary&#x27;&gt;点击我&lt;/MyButton&gt;&lt;/div&gt;&quot; 的字符串</span></span><br><span class="line">  <span class="comment">// 省略具体的字符串拼接逻辑</span></span><br><span class="line">  <span class="keyword">return</span> <span class="string">`&lt;<span class="subst">$&#123;dsl.type&#125;</span> ...&gt;...&lt;/<span class="subst">$&#123;dsl.type&#125;</span>&gt;`</span>; </span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">DynamicComponent</span> = <span class="title function_">defineComponent</span>(&#123;</span><br><span class="line">  <span class="attr">props</span>: [<span class="string">&#x27;dsl&#x27;</span>],</span><br><span class="line">  <span class="title function_">setup</span>(<span class="params">props</span>) &#123;</span><br><span class="line">    <span class="comment">// 动态生成 template 字符串</span></span><br><span class="line">    <span class="keyword">const</span> templateStr = <span class="title function_">dslToTemplate</span>(props.<span class="property">dsl</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 返回包含动态 template 的组件对象</span></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">template</span>: templateStr,</span><br><span class="line">      <span class="attr">components</span>: &#123; <span class="title class_">MyButton</span> &#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><ul><li><strong>优点</strong>：可以将传统的 HTML 字符串片段直接变活，对处理服务端下发的富文本&#x2F;旧版微前端架构比较有用。</li><li><strong>缺点</strong>：<ul><li><strong>严重警告</strong>：带来极大的安全风险（XSS 攻击），绝对不要渲染不可信的 DSL。</li><li><strong>性能较差</strong>：在浏览器里运行完整的 Vue 编译器，非常消耗性能。</li><li><strong>体积增加</strong>：打包时必须引入 Vue Compiler 版本，会增加约 100kb+ 的包体积。</li></ul></li></ul><hr><h3 id="5-构建时编译（AST-生成-Vue-SFC）"><a href="#5-构建时编译（AST-生成-Vue-SFC）" class="headerlink" title="5. 构建时编译（AST 生成 Vue SFC）"></a>5. 构建时编译（AST 生成 Vue SFC）</h3><p>这种方式不属于“运行时渲染”，而是属于“代码生成（Code Generation）”。通常用于 D2C（Design to Code，设计稿转代码）工具中。</p><ul><li><strong>实现原理</strong>：在 Node.js 环境下，读取 JSON DSL，利用 Babel 或直接拼字符串的方式，生成 <code>.vue</code> 文件（单文件组件 SFC），然后交给 Vite&#x2F;Webpack 正常打包。</li><li><strong>使用场景</strong>：比如各种低代码平台中的“导出 Vue 源码”功能，或者企业内部的 CLI 脚手架工具。</li><li><strong>优点</strong>：运行时零负担，生成的代码可二次开发。</li><li><strong>缺点</strong>：无法应对运行时 DSL 发生改变的场景（即无法做到配置即刻生效）。</li></ul><hr><h3 id="💡-总结与建议：我该选哪种？"><a href="#💡-总结与建议：我该选哪种？" class="headerlink" title="💡 总结与建议：我该选哪种？"></a>💡 总结与建议：我该选哪种？</h3><table><thead><tr><th align="left">方式</th><th align="left">性能</th><th align="left">灵活性</th><th align="left">维护难度</th><th align="left">适用场景</th></tr></thead><tbody><tr><td align="left"><strong>1. 递归动态组件</strong></td><td align="left">中等</td><td align="left">高</td><td align="left"><strong>极低</strong>（推荐）</td><td align="left">中后台动态表单、配置驱动的基础页面构建。</td></tr><tr><td align="left"><strong>2. h 渲染函数</strong></td><td align="left"><strong>最高</strong></td><td align="left"><strong>最高</strong></td><td align="left">高</td><td align="left">大型低代码平台核心渲染器、需要极致性能的复杂树状组件。</td></tr><tr><td align="left"><strong>3. JSX&#x2F;TSX</strong></td><td align="left">高</td><td align="left">高</td><td align="left">中</td><td align="left">团队有 React 背景，需要手写复杂渲染逻辑的场景。</td></tr><tr><td align="left"><strong>4. 运行时编译</strong></td><td align="left">极低</td><td align="left">中</td><td align="left">危险</td><td align="left">极特殊的富文本解析、兼容老旧系统（强烈不推荐常规业务使用）。</td></tr><tr><td align="left"><strong>5. 构建时编译</strong></td><td align="left">-</td><td align="left">-</td><td align="left">-</td><td align="left">设计稿转源码工具（D2C）、自动生成样板代码。</td></tr></tbody></table><p><strong>最佳实践闭环</strong>：<br>绝大多数现代前端业务中，如果你在做一个动态表单或中小型低代码系统，推荐使用 <strong>第1种（递归动态组件）</strong>；如果你在做一个商业级、面临复杂交互与插槽嵌套的低代码引擎，请毫不犹豫地选择 <strong>第2种（Render Function） 或 第3种（JSX）</strong>。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;将 DSL（Domain-Specific Language，领域特定语言，通常在前端表现为 JSON 或 XML 格式的数据结构）渲染为 Vue 3 组件，是&lt;strong&gt;低代码（Low-Code）、动态表单、CMS 页面可视化搭建&lt;/strong&gt;等场景的核心诉求。&lt;</summary>
      
    
    
    
    <category term="低代码" scheme="http://blog.ypintao.com/categories/%E4%BD%8E%E4%BB%A3%E7%A0%81/"/>
    
    
    <category term="低代码" scheme="http://blog.ypintao.com/tags/%E4%BD%8E%E4%BB%A3%E7%A0%81/"/>
    
  </entry>
  
  <entry>
    <title>渲染器方法详解</title>
    <link href="http://blog.ypintao.com/2026/05/09/%E5%89%8D%E7%AB%AF/%E4%BD%8E%E4%BB%A3%E7%A0%81/DSL%20%E6%B8%B2%E6%9F%93%E5%99%A8%E6%96%B9%E6%B3%95%E8%AF%A6%E8%A7%A3/"/>
    <id>http://blog.ypintao.com/2026/05/09/%E5%89%8D%E7%AB%AF/%E4%BD%8E%E4%BB%A3%E7%A0%81/DSL%20%E6%B8%B2%E6%9F%93%E5%99%A8%E6%96%B9%E6%B3%95%E8%AF%A6%E8%A7%A3/</id>
    <published>2026-05-08T16:00:00.000Z</published>
    <updated>2026-05-14T00:42:55.380Z</updated>
    
    <content type="html"><![CDATA[<h1 id="DSL-渲染器方法详解"><a href="#DSL-渲染器方法详解" class="headerlink" title="DSL 渲染器方法详解"></a>DSL 渲染器方法详解</h1><blockquote><p>基于当前项目 <code>test/demo/src</code> 源码，逐函数详细说明转换过程</p></blockquote><hr><h2 id="一、类型系统-src-core-types-ts"><a href="#一、类型系统-src-core-types-ts" class="headerlink" title="一、类型系统 (src/core/types.ts)"></a>一、类型系统 (<code>src/core/types.ts</code>)</h2><h3 id="1-1-JSExpression"><a href="#1-1-JSExpression" class="headerlink" title="1.1 JSExpression"></a>1.1 JSExpression</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">JSExpression</span> &#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;JSExpression&#x27;</span>;</span><br><span class="line">  <span class="attr">id</span>?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">value</span>: <span class="built_in">string</span>;  <span class="comment">// 如 &quot;count + 1&quot;, &quot;items.length&quot;, &quot;visible ? &#x27;show&#x27; : &#x27;hide&#x27;&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: 表示一段需要在运行时动态求值的 JavaScript 表达式。<br><code>value</code> 是字符串形式的表达式代码，渲染时通过 <code>new Function</code> 执行得到实际值。</p><p><strong>转换示例</strong>:</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></pre></td><td class="code"><pre><span class="line">输入: &#123; type: &#x27;JSExpression&#x27;, value: &#x27;count + 1&#x27; &#125;</span><br><span class="line">  ↓ parseExpression() 执行</span><br><span class="line">输出: number（如 count=5 时结果为 6）</span><br></pre></td></tr></table></figure><h3 id="1-2-JSFunction"><a href="#1-2-JSFunction" class="headerlink" title="1.2 JSFunction"></a>1.2 JSFunction</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">JSFunction</span> &#123;</span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;JSFunction&#x27;</span>;</span><br><span class="line">  <span class="attr">id</span>?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">value</span>: <span class="built_in">string</span>;  <span class="comment">// 如 &quot;function() &#123; return this.count + 1 &#125;&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: 表示一段需要在运行时动态生成的 JavaScript 函数。<br><code>value</code> 是字符串形式的函数定义代码。</p><p><strong>转换示例</strong>:</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></pre></td><td class="code"><pre><span class="line">输入: &#123; type: &#x27;JSFunction&#x27;, value: &#x27;function handleClick() &#123; this.count++ &#125;&#x27; &#125;</span><br><span class="line">  ↓ parseFunction() 执行</span><br><span class="line">输出: Function 对象，可直接调用</span><br></pre></td></tr></table></figure><h3 id="1-3-NodeSchema"><a href="#1-3-NodeSchema" class="headerlink" title="1.3 NodeSchema"></a>1.3 NodeSchema</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">NodeSchema</span> &#123;</span><br><span class="line">  <span class="attr">id</span>?: <span class="built_in">string</span>;           <span class="comment">// 节点唯一标识，设计器用</span></span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;          <span class="comment">// HTML 标签名或组件名，如 &#x27;div&#x27;, &#x27;button&#x27;, &#x27;MyComponent&#x27;</span></span><br><span class="line">  <span class="attr">from</span>?: <span class="built_in">string</span>;         <span class="comment">// 组件来源路径</span></span><br><span class="line">  <span class="attr">locked</span>?: <span class="built_in">boolean</span>;      <span class="comment">// 是否锁定（不可编辑）</span></span><br><span class="line">  <span class="attr">invisible</span>?: <span class="built_in">boolean</span>;   <span class="comment">// 是否隐藏（不渲染）</span></span><br><span class="line">  <span class="attr">props</span>?: <span class="title class_">NodeProps</span>;     <span class="comment">// 组件属性</span></span><br><span class="line">  <span class="attr">events</span>?: <span class="title class_">NodeEvents</span>;   <span class="comment">// 事件绑定</span></span><br><span class="line">  <span class="attr">directives</span>?: <span class="title class_">NodeDirective</span>[]; <span class="comment">// Vue 指令</span></span><br><span class="line">  <span class="attr">children</span>?: <span class="built_in">string</span> | <span class="title class_">JSExpression</span> | <span class="title class_">NodeSchema</span>[]; <span class="comment">// 子节点</span></span><br><span class="line">  <span class="attr">slot</span>?: <span class="built_in">string</span> | <span class="title class_">NodeSlot</span>;    <span class="comment">// 插槽信息</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: 描述页面中的每一个组件节点。<code>children</code> 可以是：</p><ul><li><code>string</code> — 纯文本内容，如 <code>&quot;Hello World&quot;</code></li><li><code>JSExpression</code> — 动态文本，如 <code>{ type: &#39;JSExpression&#39;, value: &#39;message&#39; }</code></li><li><code>NodeSchema[]</code> — 子节点数组，递归嵌套</li></ul><h3 id="1-4-BlockSchema"><a href="#1-4-BlockSchema" class="headerlink" title="1.4 BlockSchema"></a>1.4 BlockSchema</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">BlockSchema</span> &#123;</span><br><span class="line">  <span class="attr">id</span>?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;           <span class="comment">// 页面/区块名称</span></span><br><span class="line">  <span class="attr">state</span>?: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSExpression</span> | <span class="title class_">JSFunction</span>&gt;; <span class="comment">// 响应式状态</span></span><br><span class="line">  <span class="attr">computed</span>?: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSFunction</span>&gt;;              <span class="comment">// 计算属性</span></span><br><span class="line">  <span class="attr">methods</span>?: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSFunction</span>&gt;;               <span class="comment">// 方法</span></span><br><span class="line">  <span class="attr">watch</span>?: <span class="built_in">any</span>[];          <span class="comment">// 侦听器</span></span><br><span class="line">  <span class="attr">lifeCycles</span>?: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSFunction</span>&gt;;            <span class="comment">// 生命周期钩子</span></span><br><span class="line">  <span class="attr">nodes</span>?: <span class="title class_">NodeSchema</span>[];   <span class="comment">// 根节点列表</span></span><br><span class="line">  <span class="attr">css</span>?: <span class="built_in">string</span>;           <span class="comment">// CSS 样式字符串</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: 描述一个完整的页面或可复用区块，对应 Vue 的一个 SFC 组件。</p><h3 id="1-5-NodeDirective"><a href="#1-5-NodeDirective" class="headerlink" title="1.5 NodeDirective"></a>1.5 NodeDirective</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">NodeDirective</span> &#123;</span><br><span class="line">  <span class="attr">id</span>?: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span> | <span class="title class_">JSExpression</span>;  <span class="comment">// 指令名：vIf, vFor, vShow, vModel, vBind, vHtml</span></span><br><span class="line">  <span class="attr">arg</span>?: <span class="built_in">string</span> | <span class="title class_">JSExpression</span>;  <span class="comment">// 指令参数：v-model:arg</span></span><br><span class="line">  <span class="attr">modifiers</span>?: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">boolean</span>&gt;; <span class="comment">// 修饰符</span></span><br><span class="line">  <span class="attr">value</span>?: <span class="title class_">JSExpression</span>;         <span class="comment">// 指令绑定值</span></span><br><span class="line">  <span class="attr">iterator</span>?: &#123; <span class="attr">item</span>: <span class="built_in">string</span>; <span class="attr">index</span>: <span class="built_in">string</span> &#125;; <span class="comment">// v-for 的迭代变量名</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="1-6-类型守卫函数"><a href="#1-6-类型守卫函数" class="headerlink" title="1.6 类型守卫函数"></a>1.6 类型守卫函数</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">isJSExpression</span>(<span class="params"><span class="attr">data</span>: <span class="built_in">any</span></span>): data is <span class="title class_">JSExpression</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> data &amp;&amp; data.<span class="property">type</span> === <span class="string">&#x27;JSExpression&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">isJSFunction</span>(<span class="params"><span class="attr">data</span>: <span class="built_in">any</span></span>): data is <span class="title class_">JSFunction</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">typeof</span> data === <span class="string">&#x27;object&#x27;</span> &amp;&amp; data &amp;&amp; data.<span class="property">type</span> === <span class="string">&#x27;JSFunction&#x27;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: 在运行时判断一个值是否为动态表达式&#x2F;函数，用于递归遍历时区分静态值和动态值。</p><hr><h2 id="二、表达式解析-src-core-renderer-ts"><a href="#二、表达式解析-src-core-renderer-ts" class="headerlink" title="二、表达式解析 (src/core/renderer.ts)"></a>二、表达式解析 (<code>src/core/renderer.ts</code>)</h2><h3 id="2-1-parseExpression-str-self"><a href="#2-1-parseExpression-str-self" class="headerlink" title="2.1 parseExpression(str, self)"></a>2.1 parseExpression(str, self)</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">parseExpression</span>(<span class="params"><span class="attr">str</span>: <span class="title class_">JSExpression</span> | <span class="title class_">JSFunction</span>, <span class="attr">self</span>: <span class="built_in">any</span></span>) &#123;</span><br><span class="line">  <span class="comment">// 1. 获取表达式字符串</span></span><br><span class="line">  <span class="keyword">let</span> tarStr = (str.<span class="property">value</span> || <span class="string">&#x27;&#x27;</span>).<span class="title function_">trim</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 将 this 替换为 __self（因为 new Function 内不能用 this 访问上下文）</span></span><br><span class="line">  tarStr = tarStr.<span class="title function_">replace</span>(<span class="regexp">/this(\W|$)/g</span>, <span class="function">(<span class="params">_a, b</span>) =&gt;</span> <span class="string">`__self<span class="subst">$&#123;b&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3. 构建 with 语句，使 $scope 中的变量可以直接访问</span></span><br><span class="line">  <span class="keyword">const</span> code = <span class="string">`with($scope || &#123;&#125;) &#123; &quot;use strict&quot;; var __self = arguments[0]; return <span class="subst">$&#123;tarStr&#125;</span> &#125;`</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 4. 用 new Function 创建函数并执行，传入 self 作为上下文</span></span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Function</span>(<span class="string">&#x27;$scope&#x27;</span>, code)(self);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>执行原理</strong>:</p><table><thead><tr><th>步骤</th><th>说明</th></tr></thead><tbody><tr><td><code>str.value</code></td><td>原始表达式字符串，如 <code>&quot;count + 1&quot;</code></td></tr><tr><td><code>this → __self</code></td><td>替换 <code>this.count</code> 为 <code>__self.count</code></td></tr><tr><td><code>with($scope)</code></td><td>让 <code>$scope</code> 对象的属性可以直接用变量名访问</td></tr><tr><td><code>new Function(&#39;$scope&#39;, code)(self)</code></td><td>动态创建并立即执行函数</td></tr></tbody></table><p><strong>self 参数是什么</strong>: 即 <code>Context</code> 实例。Context 上有 <code>state</code>、<code>props</code>、<code>methods</code> 等属性，<br>通过 <code>with</code> 语句，表达式中可以直接写 <code>count</code> 而不是 <code>this.state.count</code>。</p><p><strong>完整执行示例</strong>:</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><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">表达式: &#123; type: &#x27;JSExpression&#x27;, value: &#x27;count + 1&#x27; &#125;</span><br><span class="line">Context: &#123; state: &#123; count: 5 &#125;, ... &#125;</span><br><span class="line"></span><br><span class="line">执行代码:</span><br><span class="line">  with($scope || &#123;&#125;) &#123;</span><br><span class="line">    &quot;use strict&quot;;</span><br><span class="line">    var __self = arguments[0];</span><br><span class="line">    return count + 1</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">$scope = Context实例，state 通过 with 展开为可访问变量</span><br><span class="line">结果: 6</span><br></pre></td></tr></table></figure><h3 id="2-2-parseFunction-str-self"><a href="#2-2-parseFunction-str-self" class="headerlink" title="2.2 parseFunction(str, self)"></a>2.2 parseFunction(str, self)</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">parseFunction</span>(<span class="params"><span class="attr">str</span>: <span class="title class_">JSFunction</span>, <span class="attr">self</span>: <span class="built_in">any</span></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> fn = <span class="title function_">parseExpression</span>(str, self);  <span class="comment">// 复用 parseExpression</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">typeof</span> fn !== <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">warn</span>(<span class="string">&#x27;parseFunction.error: not a function&#x27;</span>, str.<span class="property">value</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> fn <span class="keyword">as</span> <span class="title class_">Function</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: 与 <code>parseExpression</code> 相同的执行机制，但期望返回值是一个 Function 对象。</p><p><strong>示例</strong>:</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></pre></td><td class="code"><pre><span class="line">输入: &#123; type: &#x27;JSFunction&#x27;, value: &#x27;function() &#123; this.count++ &#125;&#x27; &#125;</span><br><span class="line">  ↓ this → __self 替换</span><br><span class="line">  ↓ with($scope) 包裹</span><br><span class="line">  ↓ new Function 执行</span><br><span class="line">输出: Function 对象，调用时 count 会自增</span><br></pre></td></tr></table></figure><hr><h2 id="三、运行时上下文-—-Context-类"><a href="#三、运行时上下文-—-Context-类" class="headerlink" title="三、运行时上下文 — Context 类"></a>三、运行时上下文 — Context 类</h2><h3 id="3-1-Context-构造"><a href="#3-1-Context-构造" class="headerlink" title="3.1 Context 构造"></a>3.1 Context 构造</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Context</span> &#123;</span><br><span class="line">  <span class="attr">state</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt; = &#123;&#125;;    <span class="comment">// Vue reactive 状态</span></span><br><span class="line">  <span class="attr">props</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt; = &#123;&#125;;    <span class="comment">// 组件 props</span></span><br><span class="line">  <span class="attr">$refs</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt; = &#123;&#125;;    <span class="comment">// 模板 ref</span></span><br><span class="line">  <span class="attr">$emit</span>: <span class="built_in">any</span> = <span class="literal">null</span>;                  <span class="comment">// 触发事件</span></span><br><span class="line">  <span class="attr">$slots</span>: <span class="built_in">any</span> = <span class="literal">null</span>;                 <span class="comment">// 插槽</span></span><br><span class="line">  <span class="attr">$watch</span>: <span class="built_in">any</span> = <span class="literal">null</span>;                 <span class="comment">// 侦听器</span></span><br><span class="line">  <span class="comment">// ... 其他 Vue 实例代理属性</span></span><br><span class="line"></span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="attr">options</span>: &#123; dsl?: BlockSchema; attrs?: <span class="built_in">any</span> &#125;</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (options.<span class="property">dsl</span>) <span class="variable language_">this</span>.<span class="property">__id</span> = options.<span class="property">dsl</span>.<span class="property">id</span> || <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">if</span> (options.<span class="property">attrs</span>) <span class="title class_">Object</span>.<span class="title function_">assign</span>(<span class="variable language_">this</span>, options.<span class="property">attrs</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: Context 是 DSL 表达式执行的”宿主环境”，代理了 Vue 组件实例的所有能力。<br>DSL 中的 <code>this.xxx</code> 实际上就是访问 Context 上的属性。</p><h3 id="3-2-ctx-setup-attrs-V-—-绑定-Vue-实例"><a href="#3-2-ctx-setup-attrs-V-—-绑定-Vue-实例" class="headerlink" title="3.2 ctx.setup(attrs, V) — 绑定 Vue 实例"></a>3.2 ctx.setup(attrs, V) — 绑定 Vue 实例</h3><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></pre></td><td class="code"><pre><span class="line"><span class="title function_">setup</span>(<span class="params"><span class="attr">attrs</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt;, <span class="attr">V</span>: <span class="built_in">any</span> = <span class="title class_">Vue</span></span>) &#123;</span><br><span class="line">  <span class="comment">// 1. 获取当前 Vue 组件实例</span></span><br><span class="line">  <span class="keyword">const</span> instance = V.<span class="title function_">getCurrentInstance</span>();</span><br><span class="line">  <span class="keyword">if</span> (!instance) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 重置缓存</span></span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">__refs</span> = &#123;&#125;;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">__refCaches</span> = &#123;&#125;;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">$refs</span> = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3. 代理 Vue 实例</span></span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">__instance</span> = instance.<span class="property">proxy</span>;</span><br><span class="line">  <span class="title class_">Object</span>.<span class="title function_">assign</span>(<span class="variable language_">this</span>, instance.<span class="property">appContext</span>.<span class="property">config</span>.<span class="property">globalProperties</span>);</span><br><span class="line">  <span class="title class_">Object</span>.<span class="title function_">assign</span>(<span class="variable language_">this</span>, attrs || &#123;&#125;);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 4. 代理 Vue 实例方法 ($emit, $nextTick, $watch 等)</span></span><br><span class="line">  <span class="variable constant_">CONTEXT_HOST</span>.<span class="title function_">forEach</span>(<span class="function">(<span class="params">name</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">this</span>[name] = <span class="variable language_">this</span>.<span class="property">__instance</span>?.[name];</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 5. 生命周期内自动更新代理</span></span><br><span class="line">  V.<span class="title function_">onMounted</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">/* 重新绑定 CONTEXT_HOST */</span> &#125;);</span><br><span class="line">  V.<span class="title function_">onUnmounted</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">/* 清理引用 */</span> &#125;);</span><br><span class="line">  V.<span class="title function_">onBeforeUpdate</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">/* 重置缓存 */</span> &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>调用时机</strong>: 在 <code>createRenderer</code> 的 <code>setup()</code> 函数内调用，使 Context 与当前 Vue 组件实例绑定。</p><h3 id="3-3-ctx-parseExpression-code-ctx-parseFunction-code"><a href="#3-3-ctx-parseExpression-code-ctx-parseFunction-code" class="headerlink" title="3.3 ctx.__parseExpression(code) &#x2F; ctx.__parseFunction(code)"></a>3.3 ctx.__parseExpression(code) &#x2F; ctx.__parseFunction(code)</h3><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></pre></td><td class="code"><pre><span class="line"><span class="title function_">__parseExpression</span>(<span class="params"><span class="attr">code</span>?: <span class="title class_">JSExpression</span> | <span class="title class_">JSFunction</span></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (!code) <span class="keyword">return</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">parseExpression</span>(code, <span class="variable language_">this</span>);  <span class="comment">// this 就是 self 参数</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">__parseFunction</span>(<span class="params"><span class="attr">code</span>?: <span class="title class_">JSFunction</span></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (!code) <span class="keyword">return</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">parseFunction</span>(code, <span class="variable language_">this</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: Context 实例方法，将自身作为执行上下文传给 parseExpression&#x2F;parseFunction。</p><h3 id="3-4-ctx-clone-context-—-克隆上下文"><a href="#3-4-ctx-clone-context-—-克隆上下文" class="headerlink" title="3.4 ctx.__clone(context) — 克隆上下文"></a>3.4 ctx.__clone(context) — 克隆上下文</h3><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></pre></td><td class="code"><pre><span class="line"><span class="title function_">__clone</span>(<span class="params"><span class="attr">context</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt; = &#123;&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> _context = &#123; ...<span class="variable language_">this</span>.<span class="property">context</span>, ...context &#125;;</span><br><span class="line">  <span class="keyword">const</span> <span class="attr">copy</span>: <span class="built_in">any</span> = &#123; ..._context, <span class="attr">context</span>: _context &#125;;</span><br><span class="line">  copy.<span class="property">context</span>.<span class="property">__proto__</span> = <span class="variable language_">this</span>.<span class="property">context</span>;</span><br><span class="line">  copy.<span class="property">__proto__</span> = <span class="variable language_">this</span>;</span><br><span class="line">  <span class="keyword">return</span> copy <span class="keyword">as</span> <span class="title class_">Context</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: 创建 Context 的浅克隆，用于 <strong>v-for 循环</strong>。每次迭代需要独立的上下文来存储 <code>item</code> 和 <code>index</code> 变量。</p><p><strong>使用场景</strong>:</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></pre></td><td class="code"><pre><span class="line">v-for=&quot;item in items&quot;</span><br><span class="line">  ↓ 每次迭代</span><br><span class="line">ctx.__clone(&#123; item: items[0], index: 0 &#125;)  // 子上下文 1</span><br><span class="line">ctx.__clone(&#123; item: items[1], index: 1 &#125;)  // 子上下文 2</span><br></pre></td></tr></table></figure><h3 id="3-5-ctx-ref-id-ref-—-处理模板引用"><a href="#3-5-ctx-ref-id-ref-—-处理模板引用" class="headerlink" title="3.5 ctx.__ref(id, ref) — 处理模板引用"></a>3.5 ctx.__ref(id, ref) — 处理模板引用</h3><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></pre></td><td class="code"><pre><span class="line"><span class="title function_">__ref</span>(<span class="params"><span class="attr">id</span>: <span class="built_in">string</span> | <span class="literal">null</span> = <span class="literal">null</span>, <span class="attr">ref</span>?: <span class="built_in">string</span> | <span class="title class_">Function</span></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (!id) <span class="keyword">return</span> <span class="literal">undefined</span>;</span><br><span class="line">  <span class="comment">// 缓存 ref 函数避免重复创建</span></span><br><span class="line">  <span class="keyword">let</span> refFunc = <span class="variable language_">this</span>.<span class="property">__refCaches</span>[id];</span><br><span class="line">  <span class="keyword">if</span> (refFunc) <span class="keyword">return</span> refFunc;</span><br><span class="line"></span><br><span class="line">  refFunc = <span class="title function_">async</span> (<span class="attr">el</span>: <span class="built_in">any</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function"><span class="params">r</span> =&gt;</span> <span class="built_in">setTimeout</span>(r, <span class="number">0</span>)); <span class="comment">// 等待 DOM 更新</span></span><br><span class="line">    <span class="keyword">let</span> dom = el?.<span class="property">$el</span> || el;</span><br><span class="line">    <span class="keyword">if</span> (id) <span class="variable language_">this</span>.<span class="property">__refs</span>[id] = el;     <span class="comment">// 存储到 Context.__refs</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">typeof</span> ref === <span class="string">&#x27;function&#x27;</span>) <span class="title function_">ref</span>(el);</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (ref) <span class="variable language_">this</span>.<span class="property">$refs</span>[ref] = el; <span class="comment">// 存储到 Vue $refs</span></span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">__refCaches</span>[id] = refFunc;</span><br><span class="line">  <span class="keyword">return</span> refFunc;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>作用</strong>: 为每个 DSL 节点创建 Vue 的 <code>ref</code> 回调函数，用于获取 DOM 元素引用。</p><hr><h2 id="四、节点渲染-—-nodeRender-核心流程"><a href="#四、节点渲染-—-nodeRender-核心流程" class="headerlink" title="四、节点渲染 — nodeRender() 核心流程"></a>四、节点渲染 — nodeRender() 核心流程</h2><h3 id="4-1-函数签名"><a href="#4-1-函数签名" class="headerlink" title="4.1 函数签名"></a>4.1 函数签名</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">nodeRender</span>(<span class="params"></span></span><br><span class="line"><span class="params">  <span class="attr">dsl</span>: <span class="title class_">NodeSchema</span>,         <span class="comment">// 当前节点 Schema</span></span></span><br><span class="line"><span class="params">  <span class="attr">ctx</span>: <span class="title class_">Context</span>,            <span class="comment">// 运行时上下文</span></span></span><br><span class="line"><span class="params">  <span class="attr">V</span>: <span class="built_in">any</span> = <span class="title class_">Vue</span>,            <span class="comment">// Vue 运行时引用</span></span></span><br><span class="line"><span class="params">  <span class="attr">brothers</span>: <span class="title class_">NodeSchema</span>[],  <span class="comment">// 兄弟节点列表（用于 v-if/v-else 链）</span></span></span><br><span class="line"><span class="params">  <span class="attr">isBranch</span>: <span class="built_in">boolean</span>,       <span class="comment">// 是否是 v-else-if/v-else 分支渲染</span></span></span><br><span class="line"><span class="params">  <span class="attr">index</span>: <span class="built_in">number</span>            <span class="comment">// 节点在兄弟中的索引</span></span></span><br><span class="line"><span class="params"></span>): <span class="built_in">any</span>  <span class="comment">// 返回 VNode | VNode[] | null</span></span><br></pre></td></tr></table></figure><h3 id="4-2-完整执行流程"><a href="#4-2-完整执行流程" class="headerlink" title="4.2 完整执行流程"></a>4.2 完整执行流程</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></pre></td><td class="code"><pre><span class="line">nodeRender(dsl, ctx, V, brothers, isBranch, index)</span><br><span class="line">  │</span><br><span class="line">  ├─ 1. 前置检查</span><br><span class="line">  │     ├─ dsl 为空或 invisible → 返回 null</span><br><span class="line">  │     └─ 非分支模式遇到 v-else-if/v-else → 返回 null（跳过）</span><br><span class="line">  │</span><br><span class="line">  ├─ 2. 提取指令</span><br><span class="line">  │     └─ getDiretives(directives) → &#123; vIf, vElseIf, vElse, vFor, vShow, vBind, vHtml, vModels &#125;</span><br><span class="line">  │</span><br><span class="line">  ├─ 3. 处理 v-if 条件链</span><br><span class="line">  │     ├─ vIf 条件为 false → 向后搜索兄弟节点的 v-else-if / v-else</span><br><span class="line">  │     ├─ 找到匹配的 v-else-if → 递归调用 nodeRender(brother, ctx, V, brothers, true)</span><br><span class="line">  │     └─ 找到 v-else → 递归调用 nodeRender(brother, ctx, V, brothers, true)</span><br><span class="line">  │</span><br><span class="line">  ├─ 4. 定义内部 render(c, seq) 函数</span><br><span class="line">  │     │</span><br><span class="line">  │     ├─ 4a. 解析属性</span><br><span class="line">  │     │     └─ parseNodeProps(id, dsl.props, c)</span><br><span class="line">  │     │         ├─ deepParseNodeProps(props, c)  // 递归处理 JSExpression</span><br><span class="line">  │     │         │   ├─ 遇到 JSExpression → c.__parseExpression() 求值</span><br><span class="line">  │     │         │   ├─ 遇到 JSFunction → c.__parseFunction() 生成函数</span><br><span class="line">  │     │         │   ├─ 遇到 Object → 递归处理每个值</span><br><span class="line">  │     │         │   └─ 遇到 Array → 递归处理每个元素</span><br><span class="line">  │     │         └─ 设置 ref 回调 c.__ref(id)</span><br><span class="line">  │     │</span><br><span class="line">  │     ├─ 4b. 解析事件</span><br><span class="line">  │     │     └─ parseNodeEvents(V, id, dsl.events, c)</span><br><span class="line">  │     │         ├─ 遍历每个事件 &#123; name, handler: JSFunction &#125;</span><br><span class="line">  │     │         ├─ c.__parseFunction(handler) → 得到实际函数</span><br><span class="line">  │     │         └─ 转为 Vue 格式: &#123; onClick: fn, onInput: fn &#125;</span><br><span class="line">  │     │</span><br><span class="line">  │     ├─ 4c. 处理特殊节点类型</span><br><span class="line">  │     │     └─ name === &#x27;slot&#x27; → 渲染 Vue slot</span><br><span class="line">  │     │</span><br><span class="line">  │     ├─ 4d. 处理指令效果</span><br><span class="line">  │     │     ├─ vBind → Object.assign(props, 解析值)  // 合并动态属性</span><br><span class="line">  │     │     ├─ vShow → 条件为 false 时设置 style.display = &#x27;none&#x27;</span><br><span class="line">  │     │     ├─ vHtml → 设置 props.innerHTML = 解析值</span><br><span class="line">  │     │     └─ vModel → 双向绑定处理（区分原生/组件）</span><br><span class="line">  │     │</span><br><span class="line">  │     ├─ 4e. 处理 v-model 双向绑定</span><br><span class="line">  │     │     ├─ 原生 HTML 标签:</span><br><span class="line">  │     │     │   props.value = 解析值</span><br><span class="line">  │     │     │   props.onInput = (v) =&gt; &#123; 变量 = v.target.value &#125;</span><br><span class="line">  │     │     └─ Vue 组件:</span><br><span class="line">  │     │         props.modelValue = 解析值</span><br><span class="line">  │     │         props[&#x27;onUpdate:modelValue&#x27;] = (v) =&gt; &#123; 变量 = v &#125;</span><br><span class="line">  │     │</span><br><span class="line">  │     ├─ 4f. 处理子节点</span><br><span class="line">  │     │     └─ childrenToSlots(V, dsl.children, c, dsl)</span><br><span class="line">  │     │         ├─ children 是 string → &#123; default: () =&gt; text &#125;</span><br><span class="line">  │     │         ├─ children 是 JSExpression → &#123; default: () =&gt; 动态文本 &#125;</span><br><span class="line">  │     │         └─ children 是 NodeSchema[] → createSlotsConfig() 分组</span><br><span class="line">  │     │             ├─ 按 slot 属性分组</span><br><span class="line">  │     │             └─ 每组递归调用 nodeRender()</span><br><span class="line">  │     │</span><br><span class="line">  │     └─ 4g. 创建 VNode</span><br><span class="line">  │           └─ Vue.createVNode(component, &#123; key, data-vtj, ...props, ...events &#125;, slots)</span><br><span class="line">  │</span><br><span class="line">  ├─ 5. 处理 v-for 循环</span><br><span class="line">  │     └─ vForRender(directive, render, ctx)</span><br><span class="line">  │         ├─ 解析数组: ctx.__parseExpression(value)</span><br><span class="line">  │         ├─ 遍历每个元素:</span><br><span class="line">  │         │   ctx.__clone(&#123; item, index &#125;) → 创建子上下文</span><br><span class="line">  │         │   render(子上下文, 序号) → 生成 VNode</span><br><span class="line">  │         └─ 返回 VNode 数组</span><br><span class="line">  │</span><br><span class="line">  └─ 6. 返回结果</span><br><span class="line">        ├─ 有 v-for → VNode 数组</span><br><span class="line">        └─ 无 v-for → 单个 VNode</span><br></pre></td></tr></table></figure><h3 id="4-3-getDiretives-—-指令分类"><a href="#4-3-getDiretives-—-指令分类" class="headerlink" title="4.3 getDiretives() — 指令分类"></a>4.3 getDiretives() — 指令分类</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">getDiretives</span>(<span class="params"><span class="attr">directives</span>: <span class="title class_">NodeDirective</span>[] = []</span>) &#123;</span><br><span class="line">  <span class="comment">// 将驼峰指令名转为标准格式：vIf → &#x27;vIf&#x27;, vFor → &#x27;vFor&#x27;</span></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">camelCase</span> = (<span class="params"><span class="attr">s</span>: <span class="built_in">string</span></span>) =&gt; s.<span class="title function_">replace</span>(<span class="regexp">/([A-Z])/g</span>, <span class="function">(<span class="params">_, c, i</span>) =&gt;</span> i === <span class="number">0</span> ? c.<span class="title function_">toLowerCase</span>() : c);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">vIf</span>: directives.<span class="title function_">find</span>(<span class="function"><span class="params">n</span> =&gt;</span> <span class="title function_">camelCase</span>(n.<span class="property">name</span>) === <span class="string">&#x27;vIf&#x27;</span>),</span><br><span class="line">    <span class="attr">vElseIf</span>: directives.<span class="title function_">find</span>(<span class="function"><span class="params">n</span> =&gt;</span> <span class="title function_">camelCase</span>(n.<span class="property">name</span>) === <span class="string">&#x27;vElseIf&#x27;</span>),</span><br><span class="line">    <span class="attr">vElse</span>: directives.<span class="title function_">find</span>(<span class="function"><span class="params">n</span> =&gt;</span> <span class="title function_">camelCase</span>(n.<span class="property">name</span>) === <span class="string">&#x27;vElse&#x27;</span>),</span><br><span class="line">    <span class="attr">vFor</span>: directives.<span class="title function_">find</span>(<span class="function"><span class="params">n</span> =&gt;</span> <span class="title function_">camelCase</span>(n.<span class="property">name</span>) === <span class="string">&#x27;vFor&#x27;</span>),</span><br><span class="line">    <span class="attr">vShow</span>: directives.<span class="title function_">find</span>(<span class="function"><span class="params">n</span> =&gt;</span> <span class="title function_">camelCase</span>(n.<span class="property">name</span>) === <span class="string">&#x27;vShow&#x27;</span>),</span><br><span class="line">    <span class="attr">vBind</span>: directives.<span class="title function_">find</span>(<span class="function"><span class="params">n</span> =&gt;</span> <span class="title function_">camelCase</span>(n.<span class="property">name</span>) === <span class="string">&#x27;vBind&#x27;</span>),</span><br><span class="line">    <span class="attr">vHtml</span>: directives.<span class="title function_">find</span>(<span class="function"><span class="params">n</span> =&gt;</span> <span class="title function_">camelCase</span>(n.<span class="property">name</span>) === <span class="string">&#x27;vHtml&#x27;</span>),</span><br><span class="line">    <span class="attr">vModels</span>: directives.<span class="title function_">filter</span>(<span class="function"><span class="params">n</span> =&gt;</span> <span class="title function_">camelCase</span>(n.<span class="property">name</span>) === <span class="string">&#x27;vModel&#x27;</span>),  <span class="comment">// 注意是 filter，可以有多个</span></span><br><span class="line">    <span class="attr">others</span>: directives.<span class="title function_">filter</span>(<span class="function"><span class="params">n</span> =&gt;</span> <span class="comment">/* 其他自定义指令 */</span>)</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>返回值</strong>: 将 directives 数组按类型分类，方便后续分别处理。</p><h3 id="4-4-deepParseNodeProps-—-深度属性解析"><a href="#4-4-deepParseNodeProps-—-深度属性解析" class="headerlink" title="4.4 deepParseNodeProps() — 深度属性解析"></a>4.4 deepParseNodeProps() — 深度属性解析</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">deepParseNodeProps</span>(<span class="params"><span class="attr">props</span>: <span class="built_in">any</span>, <span class="attr">ctx</span>: <span class="title class_">Context</span></span>): <span class="built_in">any</span> &#123;</span><br><span class="line">  <span class="comment">// 递归终止条件</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">isJSExpression</span>(props)) <span class="keyword">return</span> ctx.<span class="title function_">__parseExpression</span>(props);  <span class="comment">// 表达式 → 求值</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">isJSFunction</span>(props)) <span class="keyword">return</span> ctx.<span class="title function_">__parseFunction</span>(props);      <span class="comment">// 函数 → 生成函数</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title class_">Array</span>.<span class="title function_">isArray</span>(props)) <span class="keyword">return</span> props.<span class="title function_">map</span>(<span class="function"><span class="params">i</span> =&gt;</span> <span class="title function_">deepParseNodeProps</span>(i, ctx)); <span class="comment">// 数组 → 递归</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">typeof</span> props === <span class="string">&#x27;object&#x27;</span> &amp;&amp; props !== <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="comment">// 对象 → 递归处理每个属性值</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">keys</span>(props).<span class="title function_">reduce</span>(<span class="function">(<span class="params">r, k</span>) =&gt;</span> &#123;</span><br><span class="line">      r[k] = <span class="title function_">deepParseNodeProps</span>(props[k], ctx);</span><br><span class="line">      <span class="keyword">return</span> r;</span><br><span class="line">    &#125;, &#123;&#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> props;  <span class="comment">// 原始值直接返回</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>递归逻辑</strong>:</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><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">props: &#123;</span><br><span class="line">  class: &#123; type: &#x27;JSExpression&#x27;, value: &#x27;active ? &quot;active&quot; : &quot;&quot;&#x27; &#125;,</span><br><span class="line">  style: &#123; color: &#x27;red&#x27;, fontSize: &#123; type: &#x27;JSExpression&#x27;, value: &#x27;size + &quot;px&quot;&#x27; &#125; &#125;,</span><br><span class="line">  items: [</span><br><span class="line">    &#123; type: &#x27;JSExpression&#x27;, value: &#x27;list&#x27; &#125;,</span><br><span class="line">    &#x27;static&#x27;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br><span class="line">  ↓ deepParseNodeProps</span><br><span class="line">&#123;</span><br><span class="line">  class: &quot;active&quot;,              // JSExpression 求值</span><br><span class="line">  style: &#123; color: &#x27;red&#x27;, fontSize: &#x27;16px&#x27; &#125;,  // 嵌套递归</span><br><span class="line">  items: [[&#x27;a&#x27;,&#x27;b&#x27;], &#x27;static&#x27;]  // 数组递归</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-5-parseNodeEvents-—-事件解析"><a href="#4-5-parseNodeEvents-—-事件解析" class="headerlink" title="4.5 parseNodeEvents() — 事件解析"></a>4.5 parseNodeEvents() — 事件解析</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">parseNodeEvents</span>(<span class="params"><span class="attr">V</span>: <span class="built_in">any</span>, <span class="attr">_id</span>: <span class="built_in">string</span>, <span class="attr">events</span>: <span class="title class_">NodeEvents</span>, <span class="attr">ctx</span>: <span class="title class_">Context</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">keys</span>(events || &#123;&#125;).<span class="title function_">reduce</span>(<span class="function">(<span class="params">result, key</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> event = events[key];</span><br><span class="line">    <span class="keyword">const</span> handler = ctx.<span class="title function_">__parseFunction</span>(event.<span class="property">handler</span>); <span class="comment">// JSFunction → 真实函数</span></span><br><span class="line">    <span class="keyword">if</span> (handler) &#123;</span><br><span class="line">      <span class="comment">// click → onClick, input → onInput</span></span><br><span class="line">      result[<span class="string">&#x27;on&#x27;</span> + key.<span class="title function_">charAt</span>(<span class="number">0</span>).<span class="title function_">toUpperCase</span>() + key.<span class="title function_">slice</span>(<span class="number">1</span>)] = handler;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">  &#125;, &#123;&#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>转换规则</strong>:</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></pre></td><td class="code"><pre><span class="line">DSL:  &#123; click: &#123; handler: JSFunction &#125; &#125;</span><br><span class="line">  ↓</span><br><span class="line">Vue:  &#123; onClick: Function &#125;</span><br></pre></td></tr></table></figure><h3 id="4-6-vForRender-—-v-for-循环渲染"><a href="#4-6-vForRender-—-v-for-循环渲染" class="headerlink" title="4.6 vForRender() — v-for 循环渲染"></a>4.6 vForRender() — v-for 循环渲染</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">vForRender</span>(<span class="params"><span class="attr">directive</span>: <span class="title class_">NodeDirective</span>, <span class="attr">render</span>: (ctx: Context, seq?: <span class="built_in">number</span>) =&gt; <span class="built_in">any</span>, <span class="attr">ctx</span>: <span class="title class_">Context</span></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; value, iterator &#125; = directive;</span><br><span class="line">  <span class="keyword">const</span> &#123; item = <span class="string">&#x27;item&#x27;</span>, index = <span class="string">&#x27;index&#x27;</span> &#125; = iterator || &#123;&#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 1. 解析循环数据源</span></span><br><span class="line">  <span class="keyword">let</span> items = ctx.<span class="title function_">__parseExpression</span>(value) || [];</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 数字也可以循环: v-for=&quot;(item, index) in 3&quot; → [1, 2, 3]</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title class_">Number</span>.<span class="title function_">isInteger</span>(items)) items = <span class="keyword">new</span> <span class="title class_">Array</span>(items).<span class="title function_">fill</span>(<span class="literal">true</span>).<span class="title function_">map</span>(<span class="function">(<span class="params">_, i</span>) =&gt;</span> i + <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!<span class="title class_">Array</span>.<span class="title function_">isArray</span>(items)) <span class="keyword">return</span> [];</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3. 遍历每个元素，克隆上下文并注入循环变量</span></span><br><span class="line">  <span class="keyword">return</span> items.<span class="title function_">map</span>(<span class="function">(<span class="params">_item, _index</span>) =&gt;</span></span><br><span class="line">    <span class="title function_">render</span>(ctx.<span class="title function_">__clone</span>(&#123; [item]: _item, [index]: _index &#125;), _index)</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>执行示例</strong>:</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><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">指令: &#123; name: &#x27;vFor&#x27;, value: &#123; type: &#x27;JSExpression&#x27;, value: &#x27;items&#x27; &#125;, iterator: &#123; item: &#x27;todo&#x27;, index: &#x27;i&#x27; &#125; &#125;</span><br><span class="line">items = [&#x27;吃饭&#x27;, &#x27;睡觉&#x27;, &#x27;写代码&#x27;]</span><br><span class="line"></span><br><span class="line">  ↓ 第1次迭代</span><br><span class="line">ctx.__clone(&#123; todo: &#x27;吃饭&#x27;, i: 0 &#125;) → render(clone, 0) → VNode(&#x27;li&#x27;, &#123;&#125;, &#x27;吃饭&#x27;)</span><br><span class="line"></span><br><span class="line">  ↓ 第2次迭代</span><br><span class="line">ctx.__clone(&#123; todo: &#x27;睡觉&#x27;, i: 1 &#125;) → render(clone, 1) → VNode(&#x27;li&#x27;, &#123;&#125;, &#x27;睡觉&#x27;)</span><br><span class="line"></span><br><span class="line">  ↓ 第3次迭代</span><br><span class="line">ctx.__clone(&#123; todo: &#x27;写代码&#x27;, i: 2 &#125;) → render(clone, 2) → VNode(&#x27;li&#x27;, &#123;&#125;, &#x27;写代码&#x27;)</span><br><span class="line"></span><br><span class="line">结果: [VNode, VNode, VNode]</span><br></pre></td></tr></table></figure><h3 id="4-7-childrenToSlots-—-子节点转插槽"><a href="#4-7-childrenToSlots-—-子节点转插槽" class="headerlink" title="4.7 childrenToSlots() — 子节点转插槽"></a>4.7 childrenToSlots() — 子节点转插槽</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">childrenToSlots</span>(<span class="params"><span class="attr">V</span>: <span class="built_in">any</span>, <span class="attr">children</span>: <span class="built_in">any</span>, <span class="attr">ctx</span>: <span class="title class_">Context</span>, <span class="attr">parent</span>?: <span class="title class_">NodeSchema</span></span>): <span class="built_in">any</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (!children) <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 纯文本 → 默认插槽</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">typeof</span> children === <span class="string">&#x27;string&#x27;</span>) <span class="keyword">return</span> &#123; <span class="attr">default</span>: <span class="function">() =&gt;</span> children &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 动态文本 → 默认插槽</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title function_">isJSExpression</span>(children)) <span class="keyword">return</span> &#123; <span class="attr">default</span>: <span class="function">() =&gt;</span> <span class="title function_">toString</span>(ctx.<span class="title function_">__parseExpression</span>(children)) &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 节点数组 → 按 slot 属性分组为具名插槽</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="title class_">Array</span>.<span class="title function_">isArray</span>(children) &amp;&amp; children.<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> slots = <span class="title function_">createSlotsConfig</span>(children);</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">entries</span>(slots).<span class="title function_">reduce</span>(<span class="function">(<span class="params">result, [name, &#123; nodes &#125;]</span>) =&gt;</span> &#123;</span><br><span class="line">      result[name] = <span class="function">(<span class="params"><span class="attr">scope</span>: <span class="built_in">any</span></span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> props = parent?.<span class="property">id</span> &amp;&amp; scope ? &#123; [<span class="string">`scope_<span class="subst">$&#123;parent.id&#125;</span>`</span>]: scope &#125; : &#123;&#125;;</span><br><span class="line">        <span class="comment">// 递归渲染每个子节点</span></span><br><span class="line">        <span class="keyword">return</span> nodes.<span class="title function_">map</span>(<span class="function">(<span class="params">node, index</span>) =&gt;</span> <span class="title function_">nodeRender</span>(node, ctx.<span class="title function_">__clone</span>(props), V, nodes, <span class="literal">false</span>, index));</span><br><span class="line">      &#125;;</span><br><span class="line">      <span class="keyword">return</span> result;</span><br><span class="line">    &#125;, &#123;&#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>createSlotsConfig() 分组逻辑</strong>:</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></pre></td><td class="code"><pre><span class="line">children: [</span><br><span class="line">  &#123; name: &#x27;div&#x27;, slot: &#x27;header&#x27;, children: &#x27;标题&#x27; &#125;,    → slots.header</span><br><span class="line">  &#123; name: &#x27;p&#x27;, slot: &#x27;default&#x27;, children: &#x27;内容&#x27; &#125;,      → slots.default</span><br><span class="line">  &#123; name: &#x27;span&#x27;, slot: &#x27;footer&#x27;, children: &#x27;底部&#x27; &#125;     → slots.footer</span><br><span class="line">]</span><br></pre></td></tr></table></figure><hr><h2 id="五、状态初始化函数"><a href="#五、状态初始化函数" class="headerlink" title="五、状态初始化函数"></a>五、状态初始化函数</h2><h3 id="5-1-createState-—-创建响应式状态"><a href="#5-1-createState-—-创建响应式状态" class="headerlink" title="5.1 createState() — 创建响应式状态"></a>5.1 createState() — 创建响应式状态</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">createState</span>(<span class="params"><span class="attr">V</span>: <span class="built_in">any</span>, <span class="attr">state</span>: <span class="title class_">BlockSchema</span>[<span class="string">&#x27;state&#x27;</span>], <span class="attr">ctx</span>: <span class="title class_">Context</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> V.<span class="title function_">reactive</span>(</span><br><span class="line">    <span class="title class_">Object</span>.<span class="title function_">keys</span>(state || &#123;&#125;).<span class="title function_">reduce</span>(<span class="function">(<span class="params">result, key</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">let</span> val = state[key];</span><br><span class="line">      <span class="keyword">if</span> (<span class="title function_">isJSExpression</span>(val)) val = ctx.<span class="title function_">__parseExpression</span>(val);  <span class="comment">// 表达式求值</span></span><br><span class="line">      <span class="keyword">else</span> <span class="keyword">if</span> (<span class="title function_">isJSFunction</span>(val)) val = ctx.<span class="title function_">__parseFunction</span>(val); <span class="comment">// 函数生成</span></span><br><span class="line">      result[key] = val;</span><br><span class="line">      <span class="keyword">return</span> result;</span><br><span class="line">    &#125;, &#123;&#125;)</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>转换</strong>:</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></pre></td><td class="code"><pre><span class="line">DSL state: &#123; count: &#123; type: &#x27;JSExpression&#x27;, value: &#x27;0&#x27; &#125;, name: &#123; type: &#x27;JSExpression&#x27;, value: &#x27;&quot;Hello&quot;&#x27; &#125; &#125;</span><br><span class="line">  ↓</span><br><span class="line">Vue.reactive(&#123; count: 0, name: &#x27;Hello&#x27; &#125;)</span><br></pre></td></tr></table></figure><h3 id="5-2-createComputed-—-创建计算属性"><a href="#5-2-createComputed-—-创建计算属性" class="headerlink" title="5.2 createComputed() — 创建计算属性"></a>5.2 createComputed() — 创建计算属性</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">createComputed</span>(<span class="params"><span class="attr">V</span>: <span class="built_in">any</span>, <span class="attr">computed</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSFunction</span>&gt;, <span class="attr">ctx</span>: <span class="title class_">Context</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">entries</span>(computed ?? &#123;&#125;).<span class="title function_">reduce</span>(<span class="function">(<span class="params">result, [k, v]</span>) =&gt;</span> &#123;</span><br><span class="line">    result[k] = V.<span class="title function_">computed</span>(ctx.<span class="title function_">__parseFunction</span>(v) <span class="keyword">as</span> <span class="built_in">any</span>);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">  &#125;, &#123;&#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>转换</strong>:</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></pre></td><td class="code"><pre><span class="line">DSL computed: &#123; double: &#123; type: &#x27;JSFunction&#x27;, value: &#x27;function() &#123; return this.count * 2 &#125;&#x27; &#125; &#125;</span><br><span class="line">  ↓</span><br><span class="line">&#123; double: Vue.computed(() =&gt; count * 2) &#125;</span><br></pre></td></tr></table></figure><h3 id="5-3-createMethods-—-创建方法"><a href="#5-3-createMethods-—-创建方法" class="headerlink" title="5.3 createMethods() — 创建方法"></a>5.3 createMethods() — 创建方法</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">createMethods</span>(<span class="params"><span class="attr">methods</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSFunction</span>&gt;, <span class="attr">ctx</span>: <span class="title class_">Context</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">entries</span>(methods ?? &#123;&#125;).<span class="title function_">reduce</span>(<span class="function">(<span class="params">result, [k, v]</span>) =&gt;</span> &#123;</span><br><span class="line">    result[k] = ctx.<span class="title function_">__parseFunction</span>(v);</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">  &#125;, &#123;&#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>转换</strong>:</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></pre></td><td class="code"><pre><span class="line">DSL methods: &#123; increment: &#123; type: &#x27;JSFunction&#x27;, value: &#x27;function() &#123; this.count++ &#125;&#x27; &#125; &#125;</span><br><span class="line">  ↓</span><br><span class="line">&#123; increment: Function &#125;  // 调用时 count 自增</span><br></pre></td></tr></table></figure><h3 id="5-4-createLifeCycles-—-创建生命周期钩子"><a href="#5-4-createLifeCycles-—-创建生命周期钩子" class="headerlink" title="5.4 createLifeCycles() — 创建生命周期钩子"></a>5.4 createLifeCycles() — 创建生命周期钩子</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">createLifeCycles</span>(<span class="params"><span class="attr">lifeCycle</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="title class_">JSFunction</span>&gt;, <span class="attr">ctx</span>: <span class="title class_">Context</span></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">entries</span>(lifeCycle ?? &#123;&#125;).<span class="title function_">reduce</span>(<span class="function">(<span class="params">result, [k, v]</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> func = ctx.<span class="title function_">__parseFunction</span>(v);</span><br><span class="line">    result[k] = <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">typeof</span> func === <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function"><span class="params">r</span> =&gt;</span> <span class="built_in">setTimeout</span>(r, <span class="number">0</span>)); <span class="comment">// 延迟确保 Context 已绑定</span></span><br><span class="line">        <span class="keyword">await</span> <span class="title function_">func</span>();</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">  &#125;, &#123;&#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>支持的钩子</strong>: <code>onMounted</code>, <code>onUnmounted</code>, <code>onBeforeUpdate</code>, <code>onUpdated</code> 等。<br>这些会展开为 defineComponent 的选项式钩子。</p><h3 id="5-5-setWatches-—-设置侦听器"><a href="#5-5-setWatches-—-设置侦听器" class="headerlink" title="5.5 setWatches() — 设置侦听器"></a>5.5 setWatches() — 设置侦听器</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">setWatches</span>(<span class="params"><span class="attr">V</span>: <span class="built_in">any</span>, <span class="attr">watches</span>: <span class="built_in">any</span>[], <span class="attr">ctx</span>: <span class="title class_">Context</span></span>) &#123;</span><br><span class="line">  watches.<span class="title function_">forEach</span>(<span class="function">(<span class="params">n</span>) =&gt;</span> &#123;</span><br><span class="line">    V.<span class="title function_">watch</span>(</span><br><span class="line">      ctx.<span class="title function_">__parseExpression</span>(n.<span class="property">source</span>),     <span class="comment">// 监听源</span></span><br><span class="line">      ctx.<span class="title function_">__parseFunction</span>(n.<span class="property">handler</span>),       <span class="comment">// 回调函数</span></span><br><span class="line">      &#123; <span class="attr">deep</span>: n.<span class="property">deep</span>, <span class="attr">immediate</span>: n.<span class="property">immediate</span> &#125;  <span class="comment">// 选项</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><hr><h2 id="六、createRenderer-—-渲染器入口"><a href="#六、createRenderer-—-渲染器入口" class="headerlink" title="六、createRenderer() — 渲染器入口"></a>六、createRenderer() — 渲染器入口</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">createRenderer</span>(<span class="params"><span class="attr">options</span>: <span class="title class_">CreateRendererOptions</span></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> V = <span class="title class_">Vue</span>;</span><br><span class="line">  <span class="keyword">const</span> ctx = <span class="keyword">new</span> <span class="title class_">Context</span>(&#123; <span class="attr">dsl</span>: options.<span class="property">dsl</span> &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> renderer = V.<span class="title function_">defineComponent</span>(&#123;</span><br><span class="line">    <span class="attr">name</span>: options.<span class="property">dsl</span>?.<span class="property">name</span> || <span class="string">&#x27;DslRenderer&#x27;</span>,</span><br><span class="line">    <span class="attr">props</span>: &#123;&#125;,</span><br><span class="line"></span><br><span class="line">    <span class="comment">// async setup → 外层必须用 &lt;Suspense&gt; 包裹</span></span><br><span class="line">    <span class="keyword">async</span> <span class="title function_">setup</span>(<span class="params"></span>) &#123;</span><br><span class="line">      ctx.<span class="property">state</span> = <span class="title function_">createState</span>(V, options.<span class="property">dsl</span>?.<span class="property">state</span>, ctx);</span><br><span class="line">      <span class="keyword">const</span> computed = <span class="title function_">createComputed</span>(V, options.<span class="property">dsl</span>?.<span class="property">computed</span>, ctx);</span><br><span class="line">      <span class="keyword">const</span> methods = <span class="title function_">createMethods</span>(options.<span class="property">dsl</span>?.<span class="property">methods</span>, ctx);</span><br><span class="line">      ctx.<span class="title function_">setup</span>(&#123; ...computed, ...methods &#125;, V);</span><br><span class="line">      <span class="title function_">setWatches</span>(V, options.<span class="property">dsl</span>?.<span class="property">watch</span>, ctx);</span><br><span class="line">      <span class="keyword">return</span> &#123; <span class="attr">state</span>: ctx.<span class="property">state</span>, ...computed, ...methods &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line"></span><br><span class="line">    ...<span class="title function_">createLifeCycles</span>(options.<span class="property">dsl</span>?.<span class="property">lifeCycles</span>, ctx),</span><br><span class="line"></span><br><span class="line">    <span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (!options.<span class="property">dsl</span>?.<span class="property">nodes</span>) <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">      <span class="keyword">const</span> nodes = options.<span class="property">dsl</span>.<span class="property">nodes</span> || [];</span><br><span class="line">      <span class="keyword">if</span> (nodes.<span class="property">length</span> === <span class="number">1</span>) <span class="keyword">return</span> <span class="title function_">nodeRender</span>(nodes[<span class="number">0</span>], ctx, V, nodes);</span><br><span class="line">      <span class="keyword">return</span> V.<span class="title function_">createVNode</span>(<span class="string">&#x27;div&#x27;</span>, &#123;&#125;,</span><br><span class="line">        nodes.<span class="title function_">map</span>(<span class="function"><span class="params">child</span> =&gt;</span> <span class="title function_">nodeRender</span>(child, ctx, V, nodes))</span><br><span class="line">      );</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 class="attr">renderer</span>: V.<span class="title function_">markRaw</span>(renderer), <span class="attr">context</span>: ctx &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>返回值</strong>:</p><ul><li><code>renderer</code> — 用 <code>markRaw</code> 标记的 Vue 组件（避免被 reactive 代理）</li><li><code>context</code> — Context 实例（可用于外部操作）</li></ul><hr><h2 id="七、通信协议-src-canvas-protocol-ts"><a href="#七、通信协议-src-canvas-protocol-ts" class="headerlink" title="七、通信协议 (src/canvas/protocol.ts)"></a>七、通信协议 (<code>src/canvas/protocol.ts</code>)</h2><h3 id="7-1-消息格式"><a href="#7-1-消息格式" class="headerlink" title="7.1 消息格式"></a>7.1 消息格式</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">DesignerMessage</span> &#123;</span><br><span class="line">  <span class="attr">channel</span>: <span class="string">&#x27;__VTJ_DESIGNER__&#x27;</span>;  <span class="comment">// 频道标识，过滤无关消息</span></span><br><span class="line">  <span class="attr">type</span>: <span class="title class_">MessageType</span>;             <span class="comment">// 消息类型</span></span><br><span class="line">  <span class="attr">payload</span>?: <span class="built_in">any</span>;                 <span class="comment">// 数据载荷</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-2-消息类型"><a href="#7-2-消息类型" class="headerlink" title="7.2 消息类型"></a>7.2 消息类型</h3><table><thead><tr><th>类型</th><th>方向</th><th>说明</th><th>payload</th></tr></thead><tbody><tr><td><code>init</code></td><td>Host → Canvas</td><td>初始化 DSL</td><td>BlockSchema</td></tr><tr><td><code>updateDsl</code></td><td>双向</td><td>同步 DSL 变更</td><td>BlockSchema</td></tr><tr><td><code>select</code></td><td>Host → Canvas</td><td>设置选中节点</td><td>nodeId | null</td></tr><tr><td><code>nodeClick</code></td><td>Canvas → Host</td><td>用户点击节点</td><td>nodeId | null</td></tr><tr><td><code>dropNode</code></td><td>Host → Canvas</td><td>拖拽放置组件</td><td>{ schema, x, y }</td></tr><tr><td><code>ready</code></td><td>Canvas → Host</td><td>Canvas 就绪</td><td>无</td></tr><tr><td><code>selectedInfo</code></td><td>Canvas → Host</td><td>新增节点选中信息</td><td>{ id, node }</td></tr></tbody></table><h3 id="7-3-sendMessage-target-msg"><a href="#7-3-sendMessage-target-msg" class="headerlink" title="7.3 sendMessage(target, msg)"></a>7.3 sendMessage(target, msg)</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">sendMessage</span>(<span class="params"><span class="attr">target</span>: <span class="title class_">Window</span>, <span class="attr">msg</span>: <span class="title class_">DesignerMessage</span></span>) &#123;</span><br><span class="line">  target.<span class="title function_">postMessage</span>(msg, <span class="string">&#x27;*&#x27;</span>);  <span class="comment">// 使用 window.postMessage API</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-4-onMessage-fn-—-监听消息"><a href="#7-4-onMessage-fn-—-监听消息" class="headerlink" title="7.4 onMessage(fn) — 监听消息"></a>7.4 onMessage(fn) — 监听消息</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">onMessage</span>(<span class="params"><span class="attr">fn</span>: (msg: DesignerMessage) =&gt; <span class="built_in">void</span></span>): <span class="function">() =&gt;</span> <span class="built_in">void</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handler</span> = (<span class="params"><span class="attr">e</span>: <span class="title class_">MessageEvent</span></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (e.<span class="property">data</span> &amp;&amp; e.<span class="property">data</span>.<span class="property">channel</span> === <span class="variable constant_">CHANNEL</span>) &#123;</span><br><span class="line">      <span class="title function_">fn</span>(e.<span class="property">data</span>);  <span class="comment">// 只处理本频道的消息</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;message&#x27;</span>, handler);</span><br><span class="line">  <span class="keyword">return</span> <span class="function">() =&gt;</span> <span class="variable language_">window</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;message&#x27;</span>, handler); <span class="comment">// 返回取消函数</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="八、代码生成器-src-coder-index-ts"><a href="#八、代码生成器-src-coder-index-ts" class="headerlink" title="八、代码生成器 (src/coder/index.ts)"></a>八、代码生成器 (<code>src/coder/index.ts</code>)</h2><h3 id="8-1-generateVueSFC-dsl-—-DSL-→-Vue-SFC-源码"><a href="#8-1-generateVueSFC-dsl-—-DSL-→-Vue-SFC-源码" class="headerlink" title="8.1 generateVueSFC(dsl) — DSL → Vue SFC 源码"></a>8.1 generateVueSFC(dsl) — DSL → Vue SFC 源码</h3><p>将 BlockSchema 转为 <code>.vue</code> 单文件组件字符串：</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><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">BlockSchema</span><br><span class="line">  │</span><br><span class="line">  ├─ nodes → &lt;template&gt; 部分</span><br><span class="line">  │   └─ genNode() 递归生成 HTML 模板</span><br><span class="line">  │       ├─ props → HTML 属性</span><br><span class="line">  │       ├─ directives → v-if/v-for/v-model 等指令</span><br><span class="line">  │       ├─ events → @click/@input 等事件</span><br><span class="line">  │       └─ children → 递归子节点</span><br><span class="line">  │</span><br><span class="line">  ├─ state → &lt;script setup&gt; reactive()</span><br><span class="line">  ├─ computed → computed()</span><br><span class="line">  ├─ methods → 普通函数</span><br><span class="line">  │</span><br><span class="line">  └─ css → &lt;style scoped&gt;</span><br></pre></td></tr></table></figure><h3 id="8-2-genNode-node-indent-—-节点转模板"><a href="#8-2-genNode-node-indent-—-节点转模板" class="headerlink" title="8.2 genNode(node, indent) — 节点转模板"></a>8.2 genNode(node, indent) — 节点转模板</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></pre></td><td class="code"><pre><span class="line">NodeSchema: &#123; name: &#x27;button&#x27;, props: &#123; class: &#x27;btn&#x27; &#125;, children: &#x27;点击&#x27; &#125;</span><br><span class="line">  ↓ genNode</span><br><span class="line">&lt;button class=&quot;btn&quot;&gt;点击&lt;/button&gt;</span><br></pre></td></tr></table></figure><p><strong>指令转换</strong>:</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><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123; name: &#x27;vFor&#x27;, value: &#x27;items&#x27;, iterator: &#123; item: &#x27;item&#x27;, index: &#x27;i&#x27; &#125; &#125;</span><br><span class="line">  ↓</span><br><span class="line">v-for=&quot;(item, i) in items&quot;</span><br><span class="line"></span><br><span class="line">&#123; name: &#x27;vIf&#x27;, value: &#x27;show&#x27; &#125;</span><br><span class="line">  ↓</span><br><span class="line">v-if=&quot;show&quot;</span><br><span class="line"></span><br><span class="line">&#123; name: &#x27;vModel&#x27;, value: &#x27;text&#x27; &#125;</span><br><span class="line">  ↓</span><br><span class="line">v-model=&quot;text&quot;</span><br></pre></td></tr></table></figure><p><strong>事件转换</strong>:</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></pre></td><td class="code"><pre><span class="line">&#123; click: &#123; handler: &#123; type: &#x27;JSFunction&#x27;, value: &#x27;function() &#123; this.count++ &#125;&#x27; &#125; &#125; &#125;</span><br><span class="line">  ↓</span><br><span class="line">@click=&quot;() &#123; count++ &#125;&quot;</span><br></pre></td></tr></table></figure><h3 id="8-3-generateJSON-dsl-—-DSL-→-格式化-JSON"><a href="#8-3-generateJSON-dsl-—-DSL-→-格式化-JSON" class="headerlink" title="8.3 generateJSON(dsl) — DSL → 格式化 JSON"></a>8.3 generateJSON(dsl) — DSL → 格式化 JSON</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">generateJSON</span>(<span class="params"><span class="attr">dsl</span>: <span class="title class_">BlockSchema</span></span>): <span class="built_in">string</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(dsl, <span class="literal">null</span>, <span class="number">2</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>直接格式化输出，用于 DSL 查看和导入导出。</p><hr><h2 id="九、设计器集成"><a href="#九、设计器集成" class="headerlink" title="九、设计器集成"></a>九、设计器集成</h2><h3 id="9-1-DesignerView-→-Canvas-交互流程"><a href="#9-1-DesignerView-→-Canvas-交互流程" class="headerlink" title="9.1 DesignerView → Canvas 交互流程"></a>9.1 DesignerView → Canvas 交互流程</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></pre></td><td class="code"><pre><span class="line">DesignerView (主页面)                      CanvasApp (iframe)</span><br><span class="line">─────────────────────                      ───────────────────</span><br><span class="line">1. iframe 加载 canvas.html</span><br><span class="line">2. iframe load 事件</span><br><span class="line">3. sendToCanvas(&#x27;init&#x27;, dsl)  ─────────→  收到 init → renderDsl()</span><br><span class="line">4.                                           createRenderer(&#123; dsl &#125;)</span><br><span class="line">5.                                           &lt;Suspense&gt; 渲染</span><br><span class="line">6.                        ←───────────  sendToHost(&#x27;ready&#x27;)</span><br><span class="line">7. iframeReady = true</span><br><span class="line"></span><br><span class="line">用户拖拽物料:</span><br><span class="line">8. onDragStart(schema)                      显示蓝色虚线遮罩</span><br><span class="line">9. 鼠标移到画布上方松开</span><br><span class="line">10. onMaskMouseUp()</span><br><span class="line">11. sendToCanvas(&#x27;dropNode&#x27;, ...) ──────→  收到 dropNode</span><br><span class="line">12.                                         addNode() → renderDsl()</span><br><span class="line">13.                        ←───────────  sendToHost(&#x27;updateDsl&#x27;)</span><br><span class="line">14. eng.setDSL(dsl)</span><br><span class="line"></span><br><span class="line">用户点击节点:</span><br><span class="line">15.                                        onDocumentClick()</span><br><span class="line">16.                        ←───────────  sendToHost(&#x27;nodeClick&#x27;, id)</span><br><span class="line">17. selectedId = id</span><br><span class="line">18. 右侧 PropsPanel 显示节点配置</span><br><span class="line">19. 用户修改属性</span><br><span class="line">20. eng.updateNodeProp() → syncDslToCanvas()</span><br><span class="line">21. sendToCanvas(&#x27;updateDsl&#x27;)  ─────────→  收到 → renderDsl()</span><br></pre></td></tr></table></figure><h3 id="9-2-CanvasApp-内部渲染流程"><a href="#9-2-CanvasApp-内部渲染流程" class="headerlink" title="9.2 CanvasApp 内部渲染流程"></a>9.2 CanvasApp 内部渲染流程</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></pre></td><td class="code"><pre><span class="line">CanvasApp.renderDsl()</span><br><span class="line">  │</span><br><span class="line">  ├─ JSON.parse(JSON.stringify(dsl))  // 深拷贝去除 reactive 代理</span><br><span class="line">  │</span><br><span class="line">  ├─ createRenderer(&#123; dsl &#125;)</span><br><span class="line">  │   ├─ new Context(&#123; dsl &#125;)</span><br><span class="line">  │   └─ defineComponent(&#123; setup, render &#125;)</span><br><span class="line">  │</span><br><span class="line">  ├─ renderer.value = result.renderer  // 触发 Vue 重新渲染</span><br><span class="line">  │</span><br><span class="line">  └─ &lt;Suspense&gt;</span><br><span class="line">      └─ &lt;component :is=&quot;renderer&quot; /&gt;</span><br><span class="line">          ├─ setup() 执行</span><br><span class="line">          │   ├─ createState() → reactive state</span><br><span class="line">          │   ├─ createComputed() → computed</span><br><span class="line">          │   ├─ createMethods() → methods</span><br><span class="line">          │   └─ ctx.setup() → 绑定 Vue 实例</span><br><span class="line">          │</span><br><span class="line">          └─ render() 执行</span><br><span class="line">              └─ nodes.map(n =&gt; nodeRender(n, ctx, V, nodes))</span><br><span class="line">                  ├─ 解析 props</span><br><span class="line">                  ├─ 解析 events</span><br><span class="line">                  ├─ 处理 directives</span><br><span class="line">                  ├─ 处理 children/slots</span><br><span class="line">                  └─ createVNode() → VNode 树</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;DSL-渲染器方法详解&quot;&gt;&lt;a href=&quot;#DSL-渲染器方法详解&quot; class=&quot;headerlink&quot; title=&quot;DSL 渲染器方法详解&quot;&gt;&lt;/a&gt;DSL 渲染器方法详解&lt;/h1&gt;&lt;blockquote&gt;
&lt;p&gt;基于当前项目 &lt;code&gt;test/dem</summary>
      
    
    
    
    <category term="低代码" scheme="http://blog.ypintao.com/categories/%E4%BD%8E%E4%BB%A3%E7%A0%81/"/>
    
    
    <category term="低代码" scheme="http://blog.ypintao.com/tags/%E4%BD%8E%E4%BB%A3%E7%A0%81/"/>
    
  </entry>
  
  <entry>
    <title>自有服务器VPS 3x-Ui面板搭建安装详细教程</title>
    <link href="http://blog.ypintao.com/2026/01/03/%E4%BA%91%E6%89%8B%E6%9C%BA-%E6%97%A0%E4%BA%BA%E7%9B%B4%E6%92%AD/%E4%BB%A3%E7%90%86%E6%90%AD%E5%BB%BA/"/>
    <id>http://blog.ypintao.com/2026/01/03/%E4%BA%91%E6%89%8B%E6%9C%BA-%E6%97%A0%E4%BA%BA%E7%9B%B4%E6%92%AD/%E4%BB%A3%E7%90%86%E6%90%AD%E5%BB%BA/</id>
    <published>2026-01-03T12:26:55.739Z</published>
    <updated>2026-01-03T12:28:31.797Z</updated>
    
    <content type="html"><![CDATA[<p>自由超12&#x2F;06&#x2F;202512&#x2F;16&#x2F;2025科学上网<br>3X-Ui面板项目地址：<a href="https://github.com/MHSanaei/3x-ui">https://github.com/MHSanaei/3x-ui</a></p><span id="more"></span><p>自有服务器VPS 3x-Ui面板搭建安装详细教程01<br>准备<br>服务器：Ubuntu 22.04+&#x2F;Debian12+系统<br>域名：自购或免费域名<br>cloudflare账号<br>SSH工具：如Finalshell<br>域名解析</p><p>假设已经有域名，并且已经托管到cloudflare（推荐：Namesilo，后缀top&#x2F;xyz等2刀&#x2F;年左右，新用户优惠码：freechao，省1美元）</p><p>自有服务器VPS 3x-Ui面板搭建安装详细教程12<br>域名解析需要时间，我们优先把3x-Ui面板访问地址对应的域名在cloudflare添加DNS，推荐用二级域名</p><p>为面板访问地址开启SSL证书做准备，这样访问更加安全</p><p>①登录cloudflare：<a href="https://dash.cloudflare.com/">https://dash.cloudflare.com</a><br>②添加域名DNS指向服务器ip，关闭小云朵<br>自有服务器VPS 3x-Ui面板搭建安装详细教程02<br>④通过窗口+R，输入cmd，输入ping 域名，来查看是否解析成功</p><p>自有服务器VPS 3x-Ui面板搭建安装详细教程03</p><p>连接服务器<br>假设已经购买服务器，没有的话推荐性价比服务器Racknerd，最低10刀左右&#x2F;年：<a href="https://freechao.com/5857.html">https://freechao.com/5857.html</a></p><p>成立于 2017 年，美国老牌服务器供应商<br>优先选择洛杉矶Los Angeles DC02、圣何塞San Jose<br>终身续费同价<br>3天内免费更换1次IP，不用担心被墙<br>终身免费流量翻倍：RackNerd服务器免费流量翻倍申请教程，还没申请亏大了<br>支付方式灵活，支持 PayPal、信用卡、支付宝<br>7x24小时客服，支持中文<br>注册购买RackNerd服务器VPS账号注册购买流量翻倍详细教程<br>CPU内存SSD硬盘月流量带宽优惠价格点击购买<br>1 核1 G25 G2 TB @1Gbps$10.6&#x2F;年官网链接<br>2 核2.5 G45 G3 TB @1Gbps$18.66&#x2F;年官网链接<br>3 核4 G65 G6.5 TB @1Gbps$29.98&#x2F;年官网链接<br>5 核6 G100 G10 TB @1Gbps$44.98&#x2F;年官网链接<br>6 核8 G150 G20 TB @1Gbps$62.49&#x2F;年官网链接<br>①下载安装Finalshell：<a href="https://www.hostbuf.com/t/988.html">https://www.hostbuf.com/t/988.html</a><br>②Finalshell连接服务器<br>名称：任意自定义<br>主机：服务器的ip地址<br>端口：基本都是22<br>用户名：root<br>密码：服务器的root密码<br>自有服务器VPS 3x-Ui面板搭建安装详细教程04<br>③服务器系统更新及安装组件<br>apt update -y &amp;&amp; apt install -y curl &amp;&amp; apt install -y socat<br>自有服务器VPS 3x-Ui面板搭建安装详细教程05<br>3x-UI搭建安装部署<br>①安装3X-UI面板命令<br>bash &lt;(curl -Ls <a href="https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh">https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh</a>)<br>自有服务器VPS 3x-Ui面板搭建安装详细教程06<br>安装过程中询问设置面板端口</p><p>Would you like to customize the Panel Port settings?（If not , a random port will be applied) [y&#x2F;n]:<br>自有服务器VPS 3x-Ui面板搭建安装详细教程07<br>输入n，随机生成端口<br>输入y，自定义端口<br>如果后面无法打开面板地址，要放行端口：比如端口1234，输入命令：ufw allow 1234</p><p>②保存面板信息<br>Username：用户名<br>Password：密码<br>Port：端口<br>WebBasePath：路径<br>Aceess URL：面板访问地址<br>自有服务器VPS 3x-Ui面板搭建安装详细教程08<br>③生成面板SSL：输入命令x-ui，选择数字18<br>依次输入数字1》输入域名》》按提示输入y，绑定证书到面板</p><p>保存开启SSL之后的面板链接</p><p>自有服务器VPS 3x-Ui面板搭建安装详细教程10<br>⑤开启BBR加速<br>输入命令x-ui，选择数字23，选择两次1，Enable BBR</p><p>自有服务器VPS 3x-Ui面板搭建安装详细教程11<br>3x-UI面板设置<br>①登录面板：https:&#x2F;&#x2F;域名:端口&#x2F;根路径<br>如果没有创建SSL证书，会提示：安全警报，此连接不安全。在激活TLS进行数据保护之前，请勿输入敏感信息。</p><p>自有服务器VPS 3x-Ui面板搭建安装详细教程09<br>②点击【面板设置常规】<br>面板url根路径：让访问地址更短，前后要加&#x2F;<br>面板监听端口：可以更改<br>更改后重启面板生效</p><p>③点击【安全设定】<br>可以更改用户名和密码</p><p>④添加节点：点击入站列表》添加入站<br>组合1：vless+TCP+Reality</p><p>备注：随意<br>协议：选择vless<br>端口：随机<br>传输：选择TCP<br>安全：选reality<br>Dest（Target）&#x2F;SNI：改成yahoo.com、tesla.com、amd.com<br>点击【Get New Cert】获取公钥和私钥<br>组合2：vless+XHTTP+Reality</p><p>备注：随意<br>协议：选择vless<br>端口：随机<br>传输：选择XHTTP<br>路径：任意设置，英文<br>安全：选reality<br>Dest（Target）&#x2F;SNI：改成yahoo.com、tesla.com、amd.com<br>点击【Get New Cert】获取公钥和私钥<br>客户端配置<br>扫描二维码、或复制粘贴链接导入客户端</p><p>Windows（v2rayN）：<a href="https://github.com/2dust/v2rayN/releases">https://github.com/2dust/v2rayN/releases</a> （下载v2rayN-windows-64-SelfContained.zip版）<br>Android（v2rayNG）：<a href="https://github.com/2dust/v2rayNG/releases">https://github.com/2dust/v2rayNG/releases</a><br>Mac（v2rayN）：<a href="https://github.com/2dust/v2rayN/releases">https://github.com/2dust/v2rayN/releases</a><br>IOS（shadowrocket）：<a href="https://apps.apple.com/app/shadowrocket/id932747118">https://apps.apple.com/app/shadowrocket/id932747118</a><br>郑重声明：请合理使用科学上网，用于学习、科研、外贸等，严格遵守当地相关规定！</p><p><a href="https://youtu.be/WffyIuCy3Xw">https://youtu.be/WffyIuCy3Xw</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;自由超12&amp;#x2F;06&amp;#x2F;202512&amp;#x2F;16&amp;#x2F;2025科学上网&lt;br&gt;3X-Ui面板项目地址：&lt;a href=&quot;https://github.com/MHSanaei/3x-ui&quot;&gt;https://github.com/MHSanaei/3x-ui&lt;/a&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="云手机" scheme="http://blog.ypintao.com/categories/%E4%BA%91%E6%89%8B%E6%9C%BA/"/>
    
    <category term="无人直播，代理" scheme="http://blog.ypintao.com/categories/%E4%BA%91%E6%89%8B%E6%9C%BA/%E6%97%A0%E4%BA%BA%E7%9B%B4%E6%92%AD%EF%BC%8C%E4%BB%A3%E7%90%86/"/>
    
    
    <category term="云手机" scheme="http://blog.ypintao.com/tags/%E4%BA%91%E6%89%8B%E6%9C%BA/"/>
    
    <category term="无人直播，代理" scheme="http://blog.ypintao.com/tags/%E6%97%A0%E4%BA%BA%E7%9B%B4%E6%92%AD%EF%BC%8C%E4%BB%A3%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>购买服务器推荐链接</title>
    <link href="http://blog.ypintao.com/2026/01/03/%E4%BA%91%E6%89%8B%E6%9C%BA-%E6%97%A0%E4%BA%BA%E7%9B%B4%E6%92%AD/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    <id>http://blog.ypintao.com/2026/01/03/%E4%BA%91%E6%89%8B%E6%9C%BA-%E6%97%A0%E4%BA%BA%E7%9B%B4%E6%92%AD/%E6%9C%8D%E5%8A%A1%E5%99%A8/</id>
    <published>2026-01-03T12:26:41.374Z</published>
    <updated>2026-01-12T15:12:37.956Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://tools.ypintao.com/">https://tools.ypintao.com</a></p><p>echo <br>  “deb [arch&#x3D;$(dpkg –print-architecture) signed-by&#x3D;&#x2F;usr&#x2F;share&#x2F;keyrings&#x2F;docker-archive-keyring.gpg] <a href="https://mirrors.aliyun.com/docker-ce/linux/debian">https://mirrors.aliyun.com/docker-ce/linux/debian</a> <br>  $(lsb_release -cs) stable” | sudo tee &#x2F;etc&#x2F;apt&#x2F;sources.list.d&#x2F;docker.list &gt; &#x2F;dev&#x2F;null</p><p>  静态图片 + 背景音频（最适合 BPI-P2 Pro）<br>这是对这款开发板负担最小的方式，适合做“24小时电台”：<br>code<br>Bash<br>ffmpeg -loop 1 -i cover.jpg -i “你的音乐目录&#x2F;*.mp3” <br>-c:v libx264 -tune stillimage -c:a aac -b:a 128k <br>-pix_fmt yuv420p -shortest -f flv “rtmp:&#x2F;&#x2F;a.rtmp.youtube.com&#x2F;live2&#x2F;你的密钥”</p><p>使用 GitHub 上的自动化项目<br>如果你指的是 GitHub 上某些成熟的开源项目，通常推荐以下两个：</p><ol><li>kimbat&#x2F;youtube-live<br>重试</li></ol><p>错误原因<br> (基于 Node.js)<br>这是一个简单的推流包装，适合前端开发者。<br>安装：需要先安装 Node.js。<br>用途：通过配置文件管理直播。<br>2. Live-Stream-Radio  直播电台 (适合做 24&#x2F;7 音乐电台)<br>这个项目非常适合 BPI-P2 Pro 的音频强项。<br>它能自动读取文件夹里的音乐，随机播放，并配合一张背景图生成直播流。</p><p>获取 YouTube 直播密钥（Stream Key）的步骤非常直观，但如果你是第一次开通直播，可能需要等待 24 小时。<br>以下是详细步骤：<br>第一步：登录 YouTube Studio<br>在电脑浏览器上打开：YouTube Studio。<br>确保你已经登录了你的 Google 账号。<br>第二步：进入直播控制台<br>点击页面右上角的 “创建” (Create) 按钮（像一个摄像机图标）。<br>选择 “开始直播” (Go Live)。<br>第三步：激活直播权限（仅限第一次）<br>如果你以前从未直播过，YouTube 会提示你“请求直播权限”。<br>点击请求后，通常需要 等待 24 小时 才能审核通过。<br>如果已经开通了，你会直接进入直播控制面板。<br>第四步：获取密钥<br>在直播控制面板（YouTube Live Control Panel）中：<br>在左侧菜单确保选择了 “流” (Stream) 选项卡（不是“摄像头”）。<br>在页面中间的 “推流设置” (Stream settings) 栏目下，你会看到：<br>直播推流密钥 (Stream key)：这是一串由数字和字母组成的密码（例如：abcd-1234-efgh-5678）。<br>点击旁边的 “复制” (Copy) 按钮。<br>⚠️ 注意：绝对不要把这个密钥发给别人，任何人拿到这个密钥都能直接占用你的频道开始直播。<br>同时你也需要 推流地址 (Stream URL)：<br>就在密钥下方，通常是：rtmp:&#x2F;&#x2F;a.rtmp.youtube.com&#x2F;live2。<br>第五步：将密钥填入你的脚本<br>回到你的 BPI-P2 Pro 终端，在你之前的 start_live.sh 脚本中，将刚才复制的密钥填进去：<br>code<br>Bash</p><h1 id="修改脚本中的这一行"><a href="#修改脚本中的这一行" class="headerlink" title="修改脚本中的这一行"></a>修改脚本中的这一行</h1><p>STREAM_KEY&#x3D;”这里粘贴你刚才复制的密钥”<br>提示：如何测试是否成功？<br>在板子上运行脚本：.&#x2F;start_live.sh<br>回到 YouTube Studio 页面。<br>如果脚本运行正常，右上角的信号状态会变绿，显示 “连接状态良好”。<br>几秒钟后，预览窗口就会出现你的背景图片，并能听到声音。<br>点击页面右上角的 “开始直播” (Go Live)（如果开启了自动开始则不用点），你就正式上线了。<br>常见小问题：<br>网络问题：如果你在国内，BPI-P2 Pro 必须能够科学访问 YouTube 的 RTMP 服务器，否则推流会一直连接失败。<br>重置密钥：如果你觉得密钥泄露了，可以在 YouTube 后台点“重置”，旧密钥就会立即失效。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;a href=&quot;https://tools.ypintao.com/&quot;&gt;https://tools.ypintao.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;echo &lt;br&gt;  “deb [arch&amp;#x3D;$(dpkg –print-architecture) signed-b</summary>
      
    
    
    
    <category term="云手机" scheme="http://blog.ypintao.com/categories/%E4%BA%91%E6%89%8B%E6%9C%BA/"/>
    
    <category term="无人直播，代理" scheme="http://blog.ypintao.com/categories/%E4%BA%91%E6%89%8B%E6%9C%BA/%E6%97%A0%E4%BA%BA%E7%9B%B4%E6%92%AD%EF%BC%8C%E4%BB%A3%E7%90%86/"/>
    
    
    <category term="云手机" scheme="http://blog.ypintao.com/tags/%E4%BA%91%E6%89%8B%E6%9C%BA/"/>
    
    <category term="无人直播，代理" scheme="http://blog.ypintao.com/tags/%E6%97%A0%E4%BA%BA%E7%9B%B4%E6%92%AD%EF%BC%8C%E4%BB%A3%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>独占式文件锁定3</title>
    <link href="http://blog.ypintao.com/2025/12/28/%E5%89%8D%E7%AB%AF/electron/%E7%8B%AC%E5%8D%A0%E5%BC%8F%E6%96%87%E4%BB%B6%E9%94%81%E5%AE%9Awindowapi%E8%8E%B7%E5%8F%96%E5%8F%A5%E6%9F%84/"/>
    <id>http://blog.ypintao.com/2025/12/28/%E5%89%8D%E7%AB%AF/electron/%E7%8B%AC%E5%8D%A0%E5%BC%8F%E6%96%87%E4%BB%B6%E9%94%81%E5%AE%9Awindowapi%E8%8E%B7%E5%8F%96%E5%8F%A5%E6%9F%84/</id>
    <published>2025-12-28T15:39:07.993Z</published>
    <updated>2025-12-28T15:39:43.443Z</updated>
    
    <content type="html"><![CDATA[<p>这是一个非常硬核的系统级需求。你想要的是 <strong>强制性锁定（Mandatory Locking）</strong>。</p><p>在 Node.js&#x2F;Electron 的标准 <code>fs</code> 模块中，<code>fs.open</code> 打开的文件默认通常是允许“共享读写”的，这意味着虽然你打开了文件，外部程序（如记事本）可能仍然能读取甚至修改它（取决于 OS 及其共享标志）。</p><p>要实现“只能在你的 IDE 操作，外部完全无法重命名、删除、修改”，你需要绕过 Node.js 的 <code>fs</code>，直接调用操作系统的原生 API（Windows API）。</p><span id="more"></span><p><strong>推荐方案：使用 <code>koffi</code> (高性能 FFI 库) 调用 Windows <code>CreateFile</code> API</strong></p><p>目前没有现成的 npm 包能完美做到“递归锁定文件夹且不消耗系统资源”，因为这是操作系统层面的限制。但我可以为你写一个基于 <strong><code>koffi</code></strong>（现代版 ffi-napi，支持 Electron）的工具类，通过<strong>独占句柄</strong>来实现。</p><h3 id="核心原理"><a href="#核心原理" class="headerlink" title="核心原理"></a>核心原理</h3><p>调用 Windows 的 <code>CreateFileW</code> 函数，并将 <code>dwShareMode</code> 设置为 <code>0</code>（即不共享）。</p><ul><li>只要你的 IDE 拿着这个句柄（Handle）不释放，<strong>任何</strong>其他进程（资源管理器、Git、Notepad）尝试访问该文件都会收到 <code>Access Denied</code> 或 <code>File is being used by another process</code>。</li></ul><hr><h3 id="1-安装依赖"><a href="#1-安装依赖" class="headerlink" title="1. 安装依赖"></a>1. 安装依赖</h3><p>你需要 <code>koffi</code> 来调用 C++ API。</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">npm install koffi</span><br></pre></td></tr></table></figure><h3 id="2-实现-HandleLockManager-ts"><a href="#2-实现-HandleLockManager-ts" class="headerlink" title="2. 实现 HandleLockManager.ts"></a>2. 实现 <code>HandleLockManager.ts</code></h3><p>这个管理器负责打开文件&#x2F;文件夹的独占句柄。</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/HandleLockManager.ts</span></span><br><span class="line"><span class="keyword">import</span> koffi <span class="keyword">from</span> <span class="string">&#x27;koffi&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// === 1. 定义 Windows Kernel32 API ===</span></span><br><span class="line"><span class="keyword">const</span> lib = koffi.<span class="title function_">load</span>(<span class="string">&#x27;kernel32.dll&#x27;</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="variable constant_">GENERIC_READ</span> = <span class="number">0x80000000</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">GENERIC_WRITE</span> = <span class="number">0x40000000</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">OPEN_EXISTING</span> = <span class="number">3</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FILE_ATTRIBUTE_NORMAL</span> = <span class="number">0x80</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">FILE_FLAG_BACKUP_SEMANTICS</span> = <span class="number">0x02000000</span> <span class="comment">// 关键：允许打开文件夹句柄</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">INVALID_HANDLE_VALUE</span> = -<span class="number">1n</span> <span class="comment">// koffi 使用 BigInt 处理指针/句柄</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="title class_">CreateFileW</span> = lib.<span class="title function_">func</span>(<span class="string">&#x27;CreateFileW&#x27;</span>, <span class="string">&#x27;void*&#x27;</span>, [</span><br><span class="line">  <span class="string">&#x27;str16&#x27;</span>, <span class="comment">// lpFileName (宽字符路径)</span></span><br><span class="line">  <span class="string">&#x27;uint32&#x27;</span>, <span class="comment">// dwDesiredAccess</span></span><br><span class="line">  <span class="string">&#x27;uint32&#x27;</span>, <span class="comment">// dwShareMode (关键在这里！设为 0 表示独占)</span></span><br><span class="line">  <span class="string">&#x27;void*&#x27;</span>, <span class="comment">// lpSecurityAttributes</span></span><br><span class="line">  <span class="string">&#x27;uint32&#x27;</span>, <span class="comment">// dwCreationDisposition</span></span><br><span class="line">  <span class="string">&#x27;uint32&#x27;</span>, <span class="comment">// dwFlagsAndAttributes</span></span><br><span class="line">  <span class="string">&#x27;void*&#x27;</span> <span class="comment">// hTemplateFile</span></span><br><span class="line">])</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">CloseHandle</span> = lib.<span class="title function_">func</span>(<span class="string">&#x27;CloseHandle&#x27;</span>, <span class="string">&#x27;int&#x27;</span>, [<span class="string">&#x27;void*&#x27;</span>])</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">HandleLockManager</span> &#123;</span><br><span class="line">  <span class="comment">// 存储路径到句柄的映射</span></span><br><span class="line">  <span class="keyword">private</span> handles = <span class="keyword">new</span> <span class="title class_">Map</span>&lt;<span class="built_in">string</span>, <span class="built_in">any</span>&gt;()</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 class="doctag">@param</span> targetPath 绝对路径</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">lock</span>(<span class="params"><span class="attr">targetPath</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="property">handles</span>.<span class="title function_">has</span>(targetPath)) <span class="keyword">return</span> <span class="literal">true</span> <span class="comment">// 已经锁了</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 构造参数</span></span><br><span class="line">    <span class="comment">// dwShareMode = 0 (表示禁止其他进程 读、写、删除)</span></span><br><span class="line">    <span class="keyword">const</span> shareMode = <span class="number">0</span> </span><br><span class="line">    </span><br><span class="line">    <span class="comment">// dwFlagsAndAttributes: 如果是文件夹，必须加 BACKUP_SEMANTICS</span></span><br><span class="line">    <span class="keyword">const</span> flags = <span class="variable constant_">FILE_ATTRIBUTE_NORMAL</span> | <span class="variable constant_">FILE_FLAG_BACKUP_SEMANTICS</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 调用 Windows API</span></span><br><span class="line">    <span class="keyword">const</span> handle = <span class="title class_">CreateFileW</span>(</span><br><span class="line">      targetPath,</span><br><span class="line">      <span class="variable constant_">GENERIC_READ</span>, <span class="comment">// 我们只需要读权限来持有句柄</span></span><br><span class="line">      shareMode,    <span class="comment">// 【核心】0 = 独占！拒绝任何共享</span></span><br><span class="line">      <span class="literal">null</span>,</span><br><span class="line">      <span class="variable constant_">OPEN_EXISTING</span>,</span><br><span class="line">      flags,</span><br><span class="line">      <span class="literal">null</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 检查是否成功</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="title class_">BigInt</span>(handle) === <span class="variable constant_">INVALID_HANDLE_VALUE</span>) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`锁定失败: <span class="subst">$&#123;targetPath&#125;</span> (可能已被占用或权限不足)`</span>)</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><br><span class="line">    <span class="comment">// 4. 存入 Map</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">handles</span>.<span class="title function_">set</span>(targetPath, handle)</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`🔒 已独占锁定: <span class="subst">$&#123;targetPath&#125;</span>`</span>)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</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_">unlock</span>(<span class="params"><span class="attr">targetPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> handle = <span class="variable language_">this</span>.<span class="property">handles</span>.<span class="title function_">get</span>(targetPath)</span><br><span class="line">    <span class="keyword">if</span> (!handle) <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="title class_">CloseHandle</span>(handle)</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">handles</span>.<span class="title function_">delete</span>(targetPath)</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`🔓 已释放句柄: <span class="subst">$&#123;targetPath&#125;</span>`</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_">unlockAll</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> [path, handle] <span class="keyword">of</span> <span class="variable language_">this</span>.<span class="property">handles</span>) &#123;</span><br><span class="line">      <span class="title class_">CloseHandle</span>(handle)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">handles</span>.<span class="title function_">clear</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">const</span> handleLockManager = <span class="keyword">new</span> <span class="title class_">HandleLockManager</span>()</span><br></pre></td></tr></table></figure><h3 id="3-如何使用"><a href="#3-如何使用" class="headerlink" title="3. 如何使用"></a>3. 如何使用</h3><p>在你的主进程逻辑中集成：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// main.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; handleLockManager &#125; <span class="keyword">from</span> <span class="string">&#x27;./HandleLockManager&#x27;</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 当打开工程时</span></span><br><span class="line">ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;open-project&#x27;</span>, <span class="title function_">async</span> (e, projectDir) =&gt; &#123;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 1. 锁定根文件夹 (防止根目录被重命名/删除)</span></span><br><span class="line">  handleLockManager.<span class="title function_">lock</span>(projectDir)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 递归锁定所有子文件 (暴力锁定模式)</span></span><br><span class="line">  <span class="comment">// 注意：这会消耗系统句柄资源，文件数上万可能会有性能问题</span></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">lockRecursively</span> = (<span class="params"><span class="attr">dir</span>: <span class="built_in">string</span></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> items = fs.<span class="title function_">readdirSync</span>(dir)</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> item <span class="keyword">of</span> items) &#123;</span><br><span class="line">      <span class="keyword">if</span> ([<span class="string">&#x27;.git&#x27;</span>, <span class="string">&#x27;node_modules&#x27;</span>].<span class="title function_">includes</span>(item)) <span class="keyword">continue</span>; <span class="comment">// 跳过黑名单</span></span><br><span class="line">      </span><br><span class="line">      <span class="keyword">const</span> fullPath = path.<span class="title function_">join</span>(dir, item)</span><br><span class="line">      handleLockManager.<span class="title function_">lock</span>(fullPath)</span><br><span class="line">      </span><br><span class="line">      <span class="keyword">if</span> (fs.<span class="title function_">statSync</span>(fullPath).<span class="title function_">isDirectory</span>()) &#123;</span><br><span class="line">        <span class="title function_">lockRecursively</span>(fullPath)</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="title function_">lockRecursively</span>(projectDir)</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">return</span> &#123; <span class="attr">success</span>: <span class="literal">true</span> &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 当你的 IDE 需要修改文件时 (重命名/写入)</span></span><br><span class="line">ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;ide-write-file&#x27;</span>, <span class="title function_">async</span> (e, &#123; filePath, content &#125;) =&gt; &#123;</span><br><span class="line">  <span class="comment">// 1. 必须先解锁！因为你自己持有的 Read 句柄也会阻止你自己的 Write 操作</span></span><br><span class="line">  <span class="comment">// (除非你在 CreateFile 时指定了 WRITE 权限，但为了安全通常先解锁)</span></span><br><span class="line">  handleLockManager.<span class="title function_">unlock</span>(filePath)</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="comment">// 2. 执行操作</span></span><br><span class="line">    <span class="keyword">await</span> fs.<span class="property">promises</span>.<span class="title function_">writeFile</span>(filePath, content)</span><br><span class="line">  &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="comment">// 3. 操作完立即锁回去</span></span><br><span class="line">    handleLockManager.<span class="title function_">lock</span>(filePath)</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">app.<span class="title function_">on</span>(<span class="string">&#x27;will-quit&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  handleLockManager.<span class="title function_">unlockAll</span>()</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="4-这种方案的优缺点（必读）"><a href="#4-这种方案的优缺点（必读）" class="headerlink" title="4. 这种方案的优缺点（必读）"></a>4. 这种方案的优缺点（必读）</h3><p><strong>优点：</strong></p><ol><li><strong>绝对防御</strong>：这比 <code>icacls</code> 权限更狠。只要你的 IDE 运行着，外部用户在资源管理器里右键删除、重命名、或者用 Notepad 打开修改，Windows 都会直接弹窗“文件已在另一个程序中打开”，完全无法操作。</li><li><strong>即时生效</strong>：不需要等待，调用即锁定。</li><li><strong>崩溃安全</strong>：如果你的 Electron App 崩溃了（进程彻底结束），操作系统会自动回收所有句柄，锁会自动解开。不会像 <code>icacls</code> 那样留下永久的权限烂摊子。</li></ol><p><strong>缺点与挑战：</strong></p><ol><li><strong>文件夹锁定的局限</strong>：<ul><li>锁定一个<strong>文件夹的句柄</strong>，只能防止该文件夹本身被重命名或删除。</li><li><strong>它不能阻止外部程序修改文件夹里面的文件</strong>。</li><li><strong>解决方案</strong>：你必须像上面代码那样，<strong>递归遍历</strong>所有文件并对每一个文件都打开一个句柄。</li></ul></li><li><strong>句柄数量限制</strong>：<ul><li>如果工程有 10 万个文件，打开 10 万个句柄可能会消耗大量内存或达到系统限制。</li><li><em>建议策略</em>：只锁定当前显示的、重要的文件，或者锁定根目录和一二级目录，配合 <code>chokidar</code> 监听深层目录。或者只在“独占模式”下才递归锁。</li></ul></li><li><strong>副作用</strong>：<ul><li><strong>Git 无法使用</strong>：你在 IDE 锁着文件的时候，外部的 <code>git pull</code>、<code>git checkout</code> 会全部失败。</li><li><strong>编译无法使用</strong>：如果你在外部运行 <code>npm run build</code>，编译器会因为无法读取&#x2F;写入文件而报错。</li></ul></li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>如果你的需求是**“除了我的 IDE，谁也别想动这些文件，包括用户自己”<strong>，那么</strong>句柄锁定 (Handle Locking)** 是最正确的技术路线。</p><p>使用 <strong><code>koffi</code></strong> 调用 <code>CreateFileW</code> 并设置 <code>dwShareMode = 0</code> 是实现这一目标的终极武器。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;这是一个非常硬核的系统级需求。你想要的是 &lt;strong&gt;强制性锁定（Mandatory Locking）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在 Node.js&amp;#x2F;Electron 的标准 &lt;code&gt;fs&lt;/code&gt; 模块中，&lt;code&gt;fs.open&lt;/code&gt; 打开的文件默认通常是允许“共享读写”的，这意味着虽然你打开了文件，外部程序（如记事本）可能仍然能读取甚至修改它（取决于 OS 及其共享标志）。&lt;/p&gt;
&lt;p&gt;要实现“只能在你的 IDE 操作，外部完全无法重命名、删除、修改”，你需要绕过 Node.js 的 &lt;code&gt;fs&lt;/code&gt;，直接调用操作系统的原生 API（Windows API）。&lt;/p&gt;</summary>
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>使用trae开发</title>
    <link href="http://blog.ypintao.com/2025/12/28/%E5%89%8D%E7%AB%AF/AI/%E4%BD%BF%E7%94%A8trae%E5%BC%80%E5%8F%91/"/>
    <id>http://blog.ypintao.com/2025/12/28/%E5%89%8D%E7%AB%AF/AI/%E4%BD%BF%E7%94%A8trae%E5%BC%80%E5%8F%91/</id>
    <published>2025-12-28T05:45:05.289Z</published>
    <updated>2025-12-28T05:46:52.549Z</updated>
    
    <content type="html"><![CDATA[<h2 id="定义开发内容"><a href="#定义开发内容" class="headerlink" title="定义开发内容"></a>定义开发内容</h2><span id="more"></span><p>vibe codeing</p><p>规则驱动编程</p><p><a href="https://www.bilibili.com/video/BV1mTHHzCEaU">https://www.bilibili.com/video/BV1mTHHzCEaU</a></p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;定义开发内容&quot;&gt;&lt;a href=&quot;#定义开发内容&quot; class=&quot;headerlink&quot; title=&quot;定义开发内容&quot;&gt;&lt;/a&gt;定义开发内容&lt;/h2&gt;</summary>
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>一些常用的工具方法</title>
    <link href="http://blog.ypintao.com/2025/12/28/%E5%89%8D%E7%AB%AF/electron/%E5%B7%A5%E5%85%B7%E6%96%B9%E6%B3%95/"/>
    <id>http://blog.ypintao.com/2025/12/28/%E5%89%8D%E7%AB%AF/electron/%E5%B7%A5%E5%85%B7%E6%96%B9%E6%B3%95/</id>
    <published>2025-12-28T05:43:06.589Z</published>
    <updated>2025-12-28T05:44:17.499Z</updated>
    
    <content type="html"><![CDATA[<p>搜集一些常用的工具方法</p><span id="more"></span><h2 id="完美的-Windows-文件名校验"><a href="#完美的-Windows-文件名校验" class="headerlink" title="完美的 Windows 文件名校验"></a>完美的 Windows 文件名校验</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> isStrictWindowsFileName = (<span class="attr">name</span>: <span class="built_in">string</span>): <span class="built_in">string</span> | <span class="function"><span class="params">true</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 1. 检查非法字符</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="regexp">/[\\/:*?&quot;&lt;&gt;|]/</span>.<span class="title function_">test</span>(name)) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;名称不能包含 \\ / : * ? &quot; &lt; &gt; |&#x27;</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 检查控制字符 (0-31)</span></span><br><span class="line">  <span class="comment">// eslint-disable-next-line no-control-regex</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="regexp">/[\x00-\x1f]/</span>.<span class="title function_">test</span>(name)) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;名称不能包含控制字符&#x27;</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3. 检查保留字 (CON, PRN, AUX, NUL, COM1-9, LPT1-9)</span></span><br><span class="line">  <span class="keyword">const</span> reserved = <span class="regexp">/^(con|prn|aux|nul|com\d|lpt\d)$/i</span>;</span><br><span class="line">  <span class="keyword">if</span> (reserved.<span class="title function_">test</span>(name)) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;此名称是系统保留字，不允许使用&#x27;</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 4. 检查结尾的点或空格</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="regexp">/[. ]$/</span>.<span class="title function_">test</span>(name)) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;名称不能以空格或点结尾&#x27;</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="literal">true</span>; <span class="comment">// 通过</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;搜集一些常用的工具方法&lt;/p&gt;</summary>
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>兼容一键切换是否锁定文件夹</title>
    <link href="http://blog.ypintao.com/2025/12/21/%E5%89%8D%E7%AB%AF/electron/%E5%85%BC%E5%AE%B9%E4%B8%80%E9%94%AE%E5%88%87%E6%8D%A2%E6%98%AF%E5%90%A6%E9%94%81%E5%AE%9A%E6%96%87%E4%BB%B6%E5%A4%B9/"/>
    <id>http://blog.ypintao.com/2025/12/21/%E5%89%8D%E7%AB%AF/electron/%E5%85%BC%E5%AE%B9%E4%B8%80%E9%94%AE%E5%88%87%E6%8D%A2%E6%98%AF%E5%90%A6%E9%94%81%E5%AE%9A%E6%96%87%E4%BB%B6%E5%A4%B9/</id>
    <published>2025-12-21T13:12:20.870Z</published>
    <updated>2025-12-21T13:28:15.803Z</updated>
    
    <content type="html"><![CDATA[<h2 id="之前介绍了两种锁定文件的形式，"><a href="#之前介绍了两种锁定文件的形式，" class="headerlink" title="之前介绍了两种锁定文件的形式，"></a>之前介绍了两种锁定文件的形式，</h2><p>1、句柄占用<br>2、icacls 权限管理<br>最后我选择了icacls权限管理，因为它更加灵活，可以针对文件夹和文件分别进行权限控制。</p><p>问题，icacls管理 是可行的，但是我担心后面又不想要权限管理了（这个取决于需求）<br>研究后发现改动点其实主要在文件和文件夹的修改 保存 删除 重命名 的方式不一样。<br>icacls 都是先解锁 然后操作 然后上锁<br>正常模式就是没有解锁上锁的过程<br>目前就是要兼容这两种 一键切换。</p><span id="more"></span><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>创建了一个文件管理 src\main\ipc\file-service.ts</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/file-service.ts</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; securityManager &#125; <span class="keyword">from</span> <span class="string">&#x27;./security-manager&#x27;</span> <span class="comment">// 复用上一条回答中的 SecurityManager</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">FileService</span> &#123;</span><br><span class="line">  <span class="comment">// 当前是否处于锁定模式</span></span><br><span class="line">  <span class="keyword">private</span> isLockedMode = <span class="literal">false</span></span><br><span class="line">  <span class="comment">// 当前打开的工程根目录</span></span><br><span class="line">  <span class="keyword">private</span> currentRootPath = <span class="string">&#x27;&#x27;</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">async</span> <span class="title function_">openProject</span>(<span class="params"><span class="attr">dirPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">currentRootPath</span> = dirPath</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="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">isLockedMode</span>) &#123;</span><br><span class="line">      <span class="keyword">await</span> securityManager.<span class="title function_">lockProject</span>(dirPath)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</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">async</span> <span class="title function_">closeProject</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">isLockedMode</span>) &#123;</span><br><span class="line">      <span class="keyword">await</span> securityManager.<span class="title function_">unlockProject</span>()</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">currentRootPath</span> = <span class="string">&#x27;&#x27;</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">isLockedMode</span> = <span class="literal">false</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">async</span> <span class="title function_">toggleLockMode</span>(<span class="params"><span class="attr">locked</span>: <span class="built_in">boolean</span></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">currentRootPath</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;未打开工程&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (locked) &#123;</span><br><span class="line">      <span class="comment">// 切换到锁定模式</span></span><br><span class="line">      <span class="keyword">await</span> securityManager.<span class="title function_">lockProject</span>(<span class="variable language_">this</span>.<span class="property">currentRootPath</span>)</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">isLockedMode</span> = <span class="literal">true</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;🔄 模式切换: [独占锁定]&#x27;</span>)</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">await</span> securityManager.<span class="title function_">unlockProject</span>()</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">isLockedMode</span> = <span class="literal">false</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;🔄 模式切换: [普通模式]&#x27;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">isLockedMode</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_">getMode</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">isLockedMode</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">//  统一 CRUD 操作 (自动判断模式)</span></span><br><span class="line">  <span class="comment">// ==========================================</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">writeFile</span>(<span class="params"><span class="attr">filePath</span>: <span class="built_in">string</span>, <span class="attr">content</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="property">isLockedMode</span>) &#123;</span><br><span class="line">      <span class="comment">// 🔒 锁定模式：走特权通道 (Unlock -&gt; Write -&gt; Lock)</span></span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">await</span> securityManager.<span class="title function_">privilegedWrite</span>(filePath, content)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// 🔓 普通模式：走原生 Node.js API</span></span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">await</span> fs.<span class="title function_">writeFile</span>(filePath, content, <span class="string">&#x27;utf-8&#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="comment">//   async createFile(targetPath: string, type: &#x27;file&#x27; | &#x27;directory&#x27;) &#123;</span></span><br><span class="line"><span class="comment">//     if (this.isLockedMode) &#123;</span></span><br><span class="line"><span class="comment">//       return await securityManager.privilegedCreate(targetPath, type)</span></span><br><span class="line"><span class="comment">//     &#125; else &#123;</span></span><br><span class="line"><span class="comment">//       if (type === &#x27;directory&#x27;) &#123;</span></span><br><span class="line"><span class="comment">//         return await fs.ensureDir(targetPath)</span></span><br><span class="line"><span class="comment">//       &#125; else &#123;</span></span><br><span class="line"><span class="comment">//         return await fs.ensureFile(targetPath)</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">//   &#125;</span></span><br><span class="line">  </span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">deleteEntry</span>(<span class="params"><span class="attr">targetPath</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="property">isLockedMode</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">await</span> securityManager.<span class="title function_">privilegedDelete</span>(targetPath)</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">return</span> <span class="keyword">await</span> fs.<span class="title function_">remove</span>(targetPath)</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">async</span> <span class="title function_">renameEntry</span>(<span class="params"><span class="attr">oldPath</span>: <span class="built_in">string</span>, <span class="attr">newName</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> dir = path.<span class="title function_">dirname</span>(oldPath)</span><br><span class="line">    <span class="keyword">const</span> newPath = path.<span class="title function_">join</span>(dir, newName)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">isLockedMode</span>) &#123;</span><br><span class="line">        <span class="comment">// SecurityManager 需要补充一个 privilegedRename 方法</span></span><br><span class="line">        <span class="comment">// 逻辑同 delete/create：解锁父目录 -&gt; rename -&gt; 加锁</span></span><br><span class="line">        <span class="comment">// 这里暂时先用 unlock -&gt; rename -&gt; lock 模拟</span></span><br><span class="line">        <span class="keyword">await</span> securityManager.<span class="title function_">privilegedRename</span>(oldPath, newPath) </span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> fs.<span class="title function_">rename</span>(oldPath, newPath)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> newPath</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="comment">   */</span></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">readFile</span>(<span class="params"><span class="attr">filePath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="comment">// 统一读取，如果文件不存在会抛错</span></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> fs.<span class="title function_">readFile</span>(filePath, <span class="string">&#x27;utf-8&#x27;</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="keyword">async</span> <span class="title function_">createEntry</span>(<span class="params"><span class="attr">targetPath</span>: <span class="built_in">string</span>, <span class="attr">type</span>: <span class="string">&#x27;file&#x27;</span> | <span class="string">&#x27;directory&#x27;</span></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">isLockedMode</span>) &#123;</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>(<span class="string">`[FileService] 特权创建: <span class="subst">$&#123;targetPath&#125;</span>`</span>)</span><br><span class="line">      <span class="keyword">await</span> securityManager.<span class="title function_">privilegedCreate</span>(targetPath, <span class="keyword">type</span>)</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="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[FileService] 普通创建: <span class="subst">$&#123;targetPath&#125;</span>`</span>)</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">type</span> === <span class="string">&#x27;directory&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">await</span> fs.<span class="title function_">ensureDir</span>(targetPath)</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> fs.<span class="title function_">ensureFile</span>(targetPath)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</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">const</span> fileService = <span class="keyword">new</span> <span class="title class_">FileService</span>()</span><br></pre></td></tr></table></figure><p>然后创建一个 src\main\ipc\project-ipc.ts</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/ipc/project-ipc.ts</span></span><br><span class="line"><span class="comment">// src/main/ipc/project-ipc.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; ipcMain, dialog &#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; fileService &#125; <span class="keyword">from</span> <span class="string">&#x27;./file-service&#x27;</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span> <span class="comment">// 建议安装 fs-extra: npm install fs-extra @types/fs-extra</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"><span class="comment">// 定义文件节点类型</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">FileNode</span> &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="built_in">string</span></span><br><span class="line">    <span class="attr">path</span>: <span class="built_in">string</span></span><br><span class="line">    <span class="attr">type</span>: <span class="string">&#x27;file&#x27;</span> | <span class="string">&#x27;directory&#x27;</span></span><br><span class="line">    <span class="attr">children</span>?: <span class="title class_">FileNode</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="keyword">const</span> <span class="variable constant_">IGNORE_LIST</span> = [<span class="string">&#x27;.git&#x27;</span>, <span class="string">&#x27;node_modules&#x27;</span>, <span class="string">&#x27;.DS_Store&#x27;</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> buildFileTree = (<span class="attr">dirPath</span>: <span class="built_in">string</span>): <span class="title class_">FileNode</span>[] =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> stats = fs.<span class="title function_">statSync</span>(dirPath)</span><br><span class="line">    <span class="keyword">if</span> (!stats.<span class="title function_">isDirectory</span>()) <span class="keyword">return</span> []</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> children = fs.<span class="title function_">readdirSync</span>(dirPath)</span><br><span class="line">        .<span class="title function_">filter</span>(<span class="function"><span class="params">name</span> =&gt;</span> !<span class="variable constant_">IGNORE_LIST</span>.<span class="title function_">includes</span>(name))</span><br><span class="line">        .<span class="title function_">map</span>(<span class="function"><span class="params">name</span> =&gt;</span> &#123;</span><br><span class="line">            <span class="keyword">const</span> fullPath = path.<span class="title function_">join</span>(dirPath, name)</span><br><span class="line">            <span class="keyword">const</span> fileStats = fs.<span class="title function_">statSync</span>(fullPath)</span><br><span class="line"></span><br><span class="line">            <span class="keyword">const</span> <span class="attr">node</span>: <span class="title class_">FileNode</span> = &#123;</span><br><span class="line">                name,</span><br><span class="line">                <span class="attr">path</span>: fullPath,</span><br><span class="line">                <span class="attr">type</span>: fileStats.<span class="title function_">isDirectory</span>() ? <span class="string">&#x27;directory&#x27;</span> : <span class="string">&#x27;file&#x27;</span></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (node.<span class="property">type</span> === <span class="string">&#x27;directory&#x27;</span>) &#123;</span><br><span class="line">                node.<span class="property">children</span> = <span class="title function_">buildFileTree</span>(fullPath)</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> node</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="keyword">return</span> children.<span class="title function_">sort</span>(<span class="function">(<span class="params">a, b</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (a.<span class="property">type</span> === b.<span class="property">type</span>) <span class="keyword">return</span> a.<span class="property">name</span>.<span class="title function_">localeCompare</span>(b.<span class="property">name</span>)</span><br><span class="line">        <span class="keyword">return</span> a.<span class="property">type</span> === <span class="string">&#x27;directory&#x27;</span> ? -<span class="number">1</span> : <span class="number">1</span></span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">setupProjectIPC</span>(<span class="params"></span>) &#123;</span><br><span class="line"><span class="comment">// 2. 读取整个工程结构</span></span><br><span class="line">    ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;fs-read-project-tree&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">rootPath</span>: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (!fs.<span class="title function_">existsSync</span>(rootPath)) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Path not found&#x27;</span>)</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 读取或创建工程配置文件</span></span><br><span class="line">            <span class="comment">// const configPath = path.join(rootPath, &#x27;project.config.json&#x27;)</span></span><br><span class="line">            <span class="keyword">let</span> projectInfo = &#123; <span class="attr">name</span>: path.<span class="title function_">basename</span>(rootPath), <span class="attr">version</span>: <span class="string">&#x27;1.0.0&#x27;</span> &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// if (fs.existsSync(configPath)) &#123;</span></span><br><span class="line">            <span class="comment">//     projectInfo = await fs.readJson(configPath)</span></span><br><span class="line">            <span class="comment">// &#125; else &#123;</span></span><br><span class="line">            <span class="comment">//     // await fs.writeJson(configPath, projectInfo, &#123; spaces: 2 &#125;)</span></span><br><span class="line">            <span class="comment">//     await securityManager.privilegedWrite(configPath,JSON.stringify(projectInfo))</span></span><br><span class="line">            <span class="comment">// &#125;</span></span><br><span class="line">            <span class="comment">// console.log(&#x27;Project Info:&#x27;, projectInfo,rootPath)</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">const</span> tree = <span class="title function_">buildFileTree</span>(rootPath)</span><br><span class="line">            <span class="keyword">return</span> &#123; tree, projectInfo &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">            <span class="variable language_">console</span>.<span class="title function_">error</span>(error)</span><br><span class="line">            <span class="keyword">throw</span> error</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  <span class="comment">// 1. 打开并选择工程</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;prj-open&#x27;</span>, <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; canceled, filePaths &#125; = <span class="keyword">await</span> dialog.<span class="title function_">showOpenDialog</span>(&#123;</span><br><span class="line">      <span class="attr">properties</span>: [<span class="string">&#x27;openDirectory&#x27;</span>]</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">if</span> (canceled || !filePaths.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">const</span> path = filePaths[<span class="number">0</span>]</span><br><span class="line">    <span class="keyword">await</span> fileService.<span class="title function_">openProject</span>(path)</span><br><span class="line">    <span class="keyword">return</span> path</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 关闭</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;prj-close&#x27;</span>, <span class="function">() =&gt;</span> fileService.<span class="title function_">closeProject</span>())</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3. 切换锁定状态</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;prj-toggle-lock&#x27;</span>, <span class="function">(<span class="params">_, <span class="attr">locked</span>: <span class="built_in">boolean</span></span>) =&gt;</span> fileService.<span class="title function_">toggleLockMode</span>(locked))</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 4. 统一 CRUD</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;prj-write&#x27;</span>, <span class="function">(<span class="params">_, &#123; path, content &#125;</span>) =&gt;</span> fileService.<span class="title function_">writeFile</span>(path, content))</span><br><span class="line"><span class="comment">//   ipcMain.handle(&#x27;prj-create&#x27;, (_, &#123; path, type &#125;) =&gt; fileService.createFile(path, type))</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;prj-delete&#x27;</span>, <span class="function">(<span class="params">_, path</span>) =&gt;</span> fileService.<span class="title function_">deleteEntry</span>(path))</span><br><span class="line">    <span class="comment">// ✅ 新增：读取文件</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;prj-read&#x27;</span>, <span class="function">(<span class="params">_, path</span>) =&gt;</span> fileService.<span class="title function_">readFile</span>(path))</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ✅ 新增：创建 (注意参数解构)</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;prj-create&#x27;</span>, <span class="function">(<span class="params">_, &#123; path, <span class="keyword">type</span> &#125;</span>) =&gt;</span> fileService.<span class="title function_">createEntry</span>(path, <span class="keyword">type</span>))</span><br><span class="line">    <span class="comment">// ✅ 新增：重命名</span></span><br><span class="line">  <span class="comment">// 参数接收一个对象 &#123; oldPath, newName &#125;</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;prj-rename&#x27;</span>, <span class="function">(<span class="params">_, &#123; oldPath, newName &#125;</span>) =&gt;</span> </span><br><span class="line">    fileService.<span class="title function_">renameEntry</span>(oldPath, newName)</span><br><span class="line">  )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>创建加密的管理方法 src\main\ipc\security-manager.ts</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><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><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/ipc/security-manager.ts</span></span><br><span class="line"><span class="comment">// src/main/security-manager.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; exec &#125; <span class="keyword">from</span> <span class="string">&#x27;child_process&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; promisify &#125; <span class="keyword">from</span> <span class="string">&#x27;util&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"><span class="keyword">import</span> chokidar <span class="keyword">from</span> <span class="string">&#x27;chokidar&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> execAsync = <span class="title function_">promisify</span>(exec)</span><br><span class="line"><span class="comment">// 🚫 黑名单列表</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">IGNORE_DIRS</span> = [<span class="string">&#x27;.git&#x27;</span>, <span class="string">&#x27;node_modules&#x27;</span>, <span class="string">&#x27;dist&#x27;</span>, <span class="string">&#x27;build&#x27;</span>, <span class="string">&#x27;.vscode&#x27;</span>, <span class="string">&#x27;.idea&#x27;</span>]</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">SecurityManager</span> &#123;</span><br><span class="line">  <span class="keyword">private</span> rootPath = <span class="string">&#x27;&#x27;</span></span><br><span class="line">  <span class="keyword">private</span> <span class="attr">watcher</span>: chokidar.<span class="property">FSWatcher</span> | <span class="literal">null</span> = <span class="literal">null</span></span><br><span class="line">  <span class="keyword">private</span> isWindows = process.<span class="property">platform</span> === <span class="string">&#x27;win32&#x27;</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">async</span> <span class="title function_">lockProject</span>(<span class="params"><span class="attr">dirPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">rootPath</span> = dirPath</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`🛡️ 正在开启独占模式 (智能排除): <span class="subst">$&#123;dirPath&#125;</span>`</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 系统级权限锁定 (递归)</span></span><br><span class="line">    <span class="comment">// await this.applySystemLock(dirPath, true)</span></span><br><span class="line">        <span class="comment">// 1. 智能遍历并锁定</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">applySmartLock</span>(dirPath, <span class="literal">true</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 启动哨兵 (防止漏网之鱼)</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">startSentry</span>(dirPath)</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">async</span> <span class="title function_">unlockProject</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">rootPath</span>) <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`🔓 正在解除独占模式...`</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 关闭哨兵</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">watcher</span>) &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">watcher</span>.<span class="title function_">close</span>()</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">watcher</span> = <span class="literal">null</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 恢复系统权限</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">applySystemLock</span>(<span class="variable language_">this</span>.<span class="property">rootPath</span>, <span class="literal">false</span>, <span class="literal">true</span>) <span class="comment">// true 表示强制递归</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">rootPath</span> = <span class="string">&#x27;&#x27;</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">async</span> <span class="title function_">privilegedWrite</span>(<span class="params"><span class="attr">filePath</span>: <span class="built_in">string</span>, <span class="attr">content</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="comment">// 1. 临时解锁单个文件</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">unlockSingleFile</span>(filePath)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="comment">// 2. 写入</span></span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">writeFile</span>(filePath, content, <span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">      <span class="comment">// 3. 立即回锁</span></span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(filePath)</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">async</span> <span class="title function_">privilegedCreate</span>(<span class="params"><span class="attr">targetPath</span>: <span class="built_in">string</span>, <span class="attr">type</span>: <span class="string">&#x27;file&#x27;</span> | <span class="string">&#x27;directory&#x27;</span></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> parentDir = path.<span class="title function_">dirname</span>(targetPath)</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 解锁父目录 (才有权限创建子项)</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">unlockSingleFile</span>(parentDir)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">type</span> === <span class="string">&#x27;directory&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">await</span> fs.<span class="title function_">ensureDir</span>(targetPath)</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> fs.<span class="title function_">ensureFile</span>(targetPath)</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">// 3. 回锁父目录 &amp; 锁定新文件</span></span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(parentDir)</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(targetPath)</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">async</span> <span class="title function_">privilegedDelete</span>(<span class="params"><span class="attr">targetPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> parentDir = path.<span class="title function_">dirname</span>(targetPath)</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 解锁父目录 (才有权限删除子项) 和 目标本身</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">unlockSingleFile</span>(parentDir)</span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">unlockSingleFile</span>(targetPath) <span class="comment">// 防止自身只读无法删除</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">remove</span>(targetPath)</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">      <span class="comment">// 3. 回锁父目录</span></span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(parentDir)</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">   * 逻辑：解锁旧父目录 &amp; 文件 -&gt; 重命名 -&gt; 锁旧父目录 &amp; 锁新文件</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">privilegedRename</span>(<span class="params"><span class="attr">oldPath</span>: <span class="built_in">string</span>, <span class="attr">newPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> oldParent = path.<span class="title function_">dirname</span>(oldPath)</span><br><span class="line">    <span class="keyword">const</span> newParent = path.<span class="title function_">dirname</span>(newPath)</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 判断是否是移动到另一个文件夹 (通常重命名是在同一个文件夹，但也支持移动)</span></span><br><span class="line">    <span class="keyword">const</span> isMove = oldParent !== newParent</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 解锁阶段</span></span><br><span class="line">    <span class="comment">// 解锁旧父目录 (允许从中移除条目)</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">unlockSingleFile</span>(oldParent)</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 解锁文件本身 (允许修改其元数据/路径，Windows 上 Deny Delete 也会阻止重命名)</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">unlockSingleFile</span>(oldPath)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 如果是移动到不同目录，还需要解锁目标父目录</span></span><br><span class="line">    <span class="keyword">if</span> (isMove) &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">unlockSingleFile</span>(newParent)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="comment">// 2. 执行重命名</span></span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">rename</span>(oldPath, newPath)</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">      <span class="comment">// 3. 回锁阶段 (无论成功失败，都要尝试恢复锁)</span></span><br><span class="line">      </span><br><span class="line">      <span class="comment">// 恢复旧父目录</span></span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(oldParent)</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 如果是移动，恢复新父目录</span></span><br><span class="line">      <span class="keyword">if</span> (isMove) &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(newParent)</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">// (虽然在同一个盘符下移动通常保留 ACL，但为了保险起见，显式锁一次)</span></span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">await</span> fs.<span class="title function_">pathExists</span>(newPath)) &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(newPath)</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></span><br><span class="line">  <span class="comment">// =========================================</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><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">applySmartLock</span>(<span class="params"><span class="attr">basePath</span>: <span class="built_in">string</span>, <span class="attr">isLock</span>: <span class="built_in">boolean</span></span>) &#123;</span><br><span class="line">    <span class="comment">// 读取根目录下的第一级内容</span></span><br><span class="line">    <span class="keyword">const</span> items = <span class="keyword">await</span> fs.<span class="title function_">readdir</span>(basePath, &#123; <span class="attr">withFileTypes</span>: <span class="literal">true</span> &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> item <span class="keyword">of</span> items) &#123;</span><br><span class="line">      <span class="keyword">const</span> fullPath = path.<span class="title function_">join</span>(basePath, item.<span class="property">name</span>)</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 1. 检查是否在黑名单中</span></span><br><span class="line">      <span class="keyword">if</span> (<span class="variable constant_">IGNORE_DIRS</span>.<span class="title function_">includes</span>(item.<span class="property">name</span>)) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`⏩ 跳过忽略目录: <span class="subst">$&#123;item.name&#125;</span>`</span>)</span><br><span class="line">        <span class="keyword">continue</span></span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 2. 执行锁定</span></span><br><span class="line">      <span class="keyword">if</span> (item.<span class="title function_">isDirectory</span>()) &#123;</span><br><span class="line">        <span class="comment">// 如果是普通文件夹 (如 src)，执行递归锁定 (/T)</span></span><br><span class="line">        <span class="comment">// 这样 src 下面有 1000 个文件也会被一次性锁住，效率很高</span></span><br><span class="line">        <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">applySystemLock</span>(fullPath, isLock, <span class="literal">true</span>)</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">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(fullPath)</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">// 注意：这里我们只锁根目录的“属性”，不加 /T</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(basePath) </span><br><span class="line">  &#125;</span><br><span class="line">   <span class="comment">// 修改底层的命令执行方法，增加 recursive 参数</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">applySystemLock</span>(<span class="params"><span class="attr">targetPath</span>: <span class="built_in">string</span>, <span class="attr">isLock</span>: <span class="built_in">boolean</span>, <span class="attr">recursive</span>: <span class="built_in">boolean</span></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">isWindows</span>) &#123;</span><br><span class="line">      <span class="comment">// Windows: 使用 icacls 拒绝写入和删除</span></span><br><span class="line">      <span class="comment">// /deny Everyone:(OI)(CI)(DE,DC,WD,AD,WEA,WA)</span></span><br><span class="line">      <span class="comment">// OI: Object Inherit (对象继承)</span></span><br><span class="line">      <span class="comment">// CI: Container Inherit (容器继承)</span></span><br><span class="line">      <span class="comment">// DE: Delete, DC: Delete Child</span></span><br><span class="line">      <span class="comment">// WD: Write Data, AD: Append Data</span></span><br><span class="line">       <span class="keyword">const</span> perm = isLock </span><br><span class="line">        ? <span class="string">&#x27;/deny Everyone:(OI)(CI)(DE,DC,WD,AD,WEA,WA)&#x27;</span> </span><br><span class="line">        : <span class="string">&#x27;/remove:d Everyone&#x27;</span></span><br><span class="line">        <span class="comment">// 关键：只有 recursive 为 true 时才加 /T</span></span><br><span class="line">      <span class="keyword">const</span> recursiveFlag = recursive ? <span class="string">&#x27;/T&#x27;</span> : <span class="string">&#x27;&#x27;</span></span><br><span class="line">       <span class="keyword">const</span> cmd = <span class="string">`icacls &quot;<span class="subst">$&#123;targetPath&#125;</span>&quot; <span class="subst">$&#123;perm&#125;</span> <span class="subst">$&#123;recursiveFlag&#125;</span> /C /Q`</span></span><br><span class="line">      <span class="keyword">try</span> &#123; <span class="keyword">await</span> <span class="title function_">execAsync</span>(cmd) &#125; <span class="keyword">catch</span> (e) &#123;&#125;</span><br><span class="line">    <span class="comment">//   const cmd = isLock</span></span><br><span class="line">    <span class="comment">//     ? `icacls &quot;$&#123;targetPath&#125;&quot; /deny Everyone:(OI)(CI)(DE,DC,WD,AD,WEA,WA) /T /C /Q`</span></span><br><span class="line">    <span class="comment">//     : `icacls &quot;$&#123;targetPath&#125;&quot; /remove:d Everyone /T /C /Q`</span></span><br><span class="line">      </span><br><span class="line">    <span class="comment">//   try &#123; await execAsync(cmd) &#125; catch (e) &#123; /* 忽略部分权限错误 */ &#125;</span></span><br><span class="line"></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// Mac/Linux: 移除写权限 (chmod -R a-w)</span></span><br><span class="line">      <span class="comment">// 注意：这在 Linux 上不能完全防止拥有者删除，但能防止修改</span></span><br><span class="line">         <span class="comment">// Mac/Linux</span></span><br><span class="line">      <span class="keyword">const</span> perm = isLock ? <span class="string">&#x27;a-w&#x27;</span> : <span class="string">&#x27;u+w&#x27;</span></span><br><span class="line">      <span class="keyword">const</span> recursiveFlag = recursive ? <span class="string">&#x27;-R&#x27;</span> : <span class="string">&#x27;&#x27;</span></span><br><span class="line">      <span class="keyword">const</span> cmd = <span class="string">`chmod <span class="subst">$&#123;recursiveFlag&#125;</span> <span class="subst">$&#123;perm&#125;</span> &quot;<span class="subst">$&#123;targetPath&#125;</span>&quot;`</span></span><br><span class="line">      <span class="keyword">try</span> &#123; <span class="keyword">await</span> <span class="title function_">execAsync</span>(cmd) &#125; <span class="keyword">catch</span> (e) &#123;&#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="keyword">private</span> <span class="keyword">async</span> <span class="title function_">lockSingleFile</span>(<span class="params"><span class="attr">targetPath</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="property">isWindows</span>) &#123;</span><br><span class="line">      <span class="comment">// 锁定单个文件 (不带 /T 递归)</span></span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">execAsync</span>(<span class="string">`icacls &quot;<span class="subst">$&#123;targetPath&#125;</span>&quot; /deny Everyone:(DE,DC,WD,AD,WEA,WA) /C /Q`</span>)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">execAsync</span>(<span class="string">`chmod a-w &quot;<span class="subst">$&#123;targetPath&#125;</span>&quot;`</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">private</span> <span class="keyword">async</span> <span class="title function_">unlockSingleFile</span>(<span class="params"><span class="attr">targetPath</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="property">isWindows</span>) &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">execAsync</span>(<span class="string">`icacls &quot;<span class="subst">$&#123;targetPath&#125;</span>&quot; /remove:d Everyone /C /Q`</span>)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">execAsync</span>(<span class="string">`chmod u+w &quot;<span class="subst">$&#123;targetPath&#125;</span>&quot;`</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="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">private</span> <span class="title function_">startSentry</span>(<span class="params"><span class="attr">dirPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">watcher</span> = chokidar.<span class="title function_">watch</span>(dirPath, &#123;</span><br><span class="line">      <span class="attr">ignored</span>: <span class="regexp">/(^|[\/\\])\../</span>,</span><br><span class="line">      <span class="attr">ignoreInitial</span>: <span class="literal">true</span>, <span class="comment">// 不处理已存在的文件</span></span><br><span class="line">      <span class="attr">persistent</span>: <span class="literal">true</span></span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 如果检测到有新文件/文件夹被外部创建（此时我们没有调用 privilegedCreate）</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="variable language_">this</span>.<span class="property">watcher</span>.<span class="title function_">on</span>(<span class="string">&#x27;add&#x27;</span>, <span class="title function_">async</span> (filePath) =&gt; &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">warn</span>(<span class="string">`🚨 检测到外部新增文件: <span class="subst">$&#123;filePath&#125;</span>，正在强制锁定...`</span>)</span><br><span class="line">        <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(filePath)</span><br><span class="line">    &#125;)</span><br><span class="line">    </span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">watcher</span>.<span class="title function_">on</span>(<span class="string">&#x27;addDir&#x27;</span>, <span class="title function_">async</span> (p) =&gt; &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(p)</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="keyword">export</span> <span class="keyword">const</span> securityManager = <span class="keyword">new</span> <span class="title class_">SecurityManager</span>()</span><br></pre></td></tr></table></figure><p>在 src\main\index.ts 中引入 SecurityManager 并初始化 projectIPC</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/index.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; securityManager &#125; <span class="keyword">from</span> <span class="string">&#x27;./ipc/security-manager&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123;setupProjectIPC  &#125; <span class="keyword">from</span> <span class="string">&#x27;./ipc/project-ipc&#x27;</span></span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">whenReady</span>().<span class="title function_">then</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// Set app user model id for windows</span></span><br><span class="line">  electronApp.<span class="title function_">setAppUserModelId</span>(<span class="string">&#x27;com.electron&#x27;</span>)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Default open or close DevTools by F12 in development</span></span><br><span class="line">  <span class="comment">// and ignore CommandOrControl + R in production.</span></span><br><span class="line">  <span class="comment">// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils</span></span><br><span class="line">  app.<span class="title function_">on</span>(<span class="string">&#x27;browser-window-created&#x27;</span>, <span class="function">(<span class="params">_, <span class="variable language_">window</span></span>) =&gt;</span> &#123;</span><br><span class="line">    optimizer.<span class="title function_">watchWindowShortcuts</span>(<span class="variable language_">window</span>)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">  <span class="comment">// setupWindowIPC()</span></span><br><span class="line">  <span class="comment">// setupLockIPC()</span></span><br><span class="line">  <span class="comment">// setupSecurityIPC()</span></span><br><span class="line">  <span class="title function_">setupProjectIPC</span>()</span><br><span class="line">  <span class="title function_">createWindow</span>()</span><br><span class="line"></span><br><span class="line">  app.<span class="title function_">on</span>(<span class="string">&#x27;activate&#x27;</span>, <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">    <span class="comment">// On macOS it&#x27;s common to re-create a window in the app when the</span></span><br><span class="line">    <span class="comment">// dock icon is clicked and there are no other windows open.</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="title class_">BrowserWindow</span>.<span class="title function_">getAllWindows</span>().<span class="property">length</span> === <span class="number">0</span>) <span class="title function_">createWindow</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="comment">// Quit when all windows are closed, except on macOS. There, it&#x27;s common</span></span><br><span class="line"><span class="comment">// for applications and their menu bar to stay active until the user quits</span></span><br><span class="line"><span class="comment">// explicitly with Cmd + Q.</span></span><br><span class="line">app.<span class="title function_">on</span>(<span class="string">&#x27;window-all-closed&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (process.<span class="property">platform</span> !== <span class="string">&#x27;darwin&#x27;</span>) &#123;</span><br><span class="line">    app.<span class="title function_">quit</span>()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">on</span>(<span class="string">&#x27;will-quit&#x27;</span>, <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="comment">// 确保退出时解锁，否则你的文件夹会被锁死，需要管理员权限修复</span></span><br><span class="line">  <span class="keyword">await</span> securityManager.<span class="title function_">unlockProject</span>()</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>然后是 preload.ts 文件，用于在渲染进程中注入一些全局变量</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/preload/index.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; contextBridge, ipcRenderer &#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; electronAPI &#125; <span class="keyword">from</span> <span class="string">&#x27;@electron-toolkit/preload&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Custom APIs for renderer</span></span><br><span class="line"><span class="comment">// 自定义 API</span></span><br><span class="line"><span class="keyword">const</span> api = &#123;</span><br><span class="line">  <span class="comment">// 调用主进程检测坐标</span></span><br><span class="line">  <span class="attr">checkCursorOutside</span>: <span class="function">() =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;check-cursor-outside-window&#x27;</span>),</span><br><span class="line">  <span class="attr">activateCurrentWindow</span>: <span class="function">() =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;activate-current-window&#x27;</span>),</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> projectAPI = &#123;</span><br><span class="line">  <span class="attr">open</span>: <span class="function">() =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;prj-open&#x27;</span>),</span><br><span class="line">  <span class="attr">close</span>: <span class="function">() =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;prj-close&#x27;</span>),</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 切换模式：传 true 开启独占，传 false 关闭</span></span><br><span class="line">  <span class="attr">toggleLock</span>: <span class="function">(<span class="params"><span class="attr">locked</span>: <span class="built_in">boolean</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;prj-toggle-lock&#x27;</span>, locked),</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 统一的操作接口</span></span><br><span class="line">  <span class="attr">writeFile</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span>, <span class="attr">content</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;prj-write&#x27;</span>, &#123; path, content &#125;),</span><br><span class="line">  <span class="attr">create</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span>, <span class="attr">type</span>: <span class="string">&#x27;file&#x27;</span> | <span class="string">&#x27;directory&#x27;</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;prj-create&#x27;</span>, &#123; path, <span class="keyword">type</span> &#125;),</span><br><span class="line">  <span class="attr">delete</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;prj-delete&#x27;</span>, path),</span><br><span class="line"></span><br><span class="line">  <span class="attr">readDir</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;fs-read-project-tree&#x27;</span>, path),</span><br><span class="line">  <span class="comment">// ✅ 新增</span></span><br><span class="line">  <span class="attr">rename</span>: <span class="function">(<span class="params"><span class="attr">oldPath</span>: <span class="built_in">string</span>, <span class="attr">newName</span>: <span class="built_in">string</span></span>) =&gt;</span></span><br><span class="line">    ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;prj-rename&#x27;</span>, &#123; oldPath, newName &#125;),</span><br><span class="line">  <span class="attr">readFile</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;prj-read&#x27;</span>, path),</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Use `contextBridge` APIs to expose Electron APIs to</span></span><br><span class="line"><span class="comment">// renderer only if context isolation is enabled, otherwise</span></span><br><span class="line"><span class="comment">// just add to the DOM global.</span></span><br><span class="line"><span class="keyword">if</span> (process.<span class="property">contextIsolated</span>) &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">&#x27;electron&#x27;</span>, electronAPI)</span><br><span class="line">    contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">&#x27;api&#x27;</span>, api)</span><br><span class="line">    contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">&#x27;lockAPI&#x27;</span>, lockAPI)</span><br><span class="line">    contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">&#x27;fsAPI&#x27;</span>, fsAPI)</span><br><span class="line">    contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">&#x27;securityAPI&#x27;</span>, securityAPI)</span><br><span class="line">    contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">&#x27;projectAPI&#x27;</span>, projectAPI)</span><br><span class="line"></span><br><span class="line">  &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">error</span>(error)</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">// @ts-ignore (define in dts)</span></span><br><span class="line">  <span class="variable language_">window</span>.<span class="property">electron</span> = electronAPI</span><br><span class="line">  <span class="comment">// @ts-ignore (define in dts)</span></span><br><span class="line">  <span class="variable language_">window</span>.<span class="property">api</span> = api</span><br><span class="line">  <span class="comment">// @ts-ignore</span></span><br><span class="line">  <span class="variable language_">window</span>.<span class="property">projectAPI</span> = projectAPI</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对应的ts文件</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/preload/index.d.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ElectronAPI</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@electron-toolkit/preload&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">ProjectAPI</span> &#123;</span><br><span class="line">  <span class="attr">open</span>: <span class="function">() =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">string</span> | <span class="literal">null</span>&gt;;</span><br><span class="line">  <span class="attr">close</span>: <span class="function">() =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">  <span class="attr">toggleLock</span>: <span class="function">(<span class="params"><span class="attr">locked</span>: <span class="built_in">boolean</span></span>) =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">boolean</span>&gt;;</span><br><span class="line">  <span class="attr">writeFile</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span>, <span class="attr">content</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">  <span class="attr">create</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span>, <span class="attr">type</span>: <span class="string">&#x27;file&#x27;</span> | <span class="string">&#x27;directory&#x27;</span></span>) =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">  <span class="attr">delete</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">  <span class="attr">readDir</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="title class_">Promise</span>&lt;&#123; <span class="attr">tree</span>: <span class="title class_">FileNode</span>[], <span class="attr">projectInfo</span>: <span class="built_in">string</span> &#125;&gt;;</span><br><span class="line">  <span class="attr">rename</span>: <span class="function">(<span class="params"><span class="attr">oldPath</span>: <span class="built_in">string</span>, <span class="attr">newName</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">string</span>&gt;;</span><br><span class="line">  <span class="attr">readFile</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">string</span>&gt;;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">declare</span> <span class="variable language_">global</span> &#123;</span><br><span class="line">  <span class="keyword">interface</span> <span class="title class_">Window</span> &#123;</span><br><span class="line">    <span class="attr">electron</span>: <span class="title class_">ElectronAPI</span></span><br><span class="line">    <span class="attr">api</span>: &#123;</span><br><span class="line">      <span class="attr">checkCursorOutside</span>: <span class="function">() =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">boolean</span>&gt;</span><br><span class="line">      <span class="attr">activateCurrentWindow</span>: <span class="function">() =&gt;</span> <span class="title class_">Promise</span>&lt;<span class="built_in">boolean</span>&gt;</span><br><span class="line">    &#125;,</span><br><span class="line"></span><br><span class="line">    <span class="attr">projectAPI</span>:<span class="title class_">ProjectAPI</span></span><br><span class="line"></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>新建LockEditor.vue文件，用于编辑锁定文件</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><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><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br><span class="line">273</span><br><span class="line">274</span><br><span class="line">275</span><br><span class="line">276</span><br><span class="line">277</span><br><span class="line">278</span><br><span class="line">279</span><br><span class="line">280</span><br><span class="line">281</span><br><span class="line">282</span><br><span class="line">283</span><br><span class="line">284</span><br><span class="line">285</span><br><span class="line">286</span><br><span class="line">287</span><br><span class="line">288</span><br><span class="line">289</span><br><span class="line">290</span><br><span class="line">291</span><br><span class="line">292</span><br><span class="line">293</span><br><span class="line">294</span><br><span class="line">295</span><br><span class="line">296</span><br><span class="line">297</span><br><span class="line">298</span><br><span class="line">299</span><br><span class="line">300</span><br><span class="line">301</span><br><span class="line">302</span><br><span class="line">303</span><br><span class="line">304</span><br><span class="line">305</span><br><span class="line">306</span><br><span class="line">307</span><br><span class="line">308</span><br><span class="line">309</span><br><span class="line">310</span><br><span class="line">311</span><br><span class="line">312</span><br><span class="line">313</span><br><span class="line">314</span><br><span class="line">315</span><br><span class="line">316</span><br><span class="line">317</span><br><span class="line">318</span><br><span class="line">319</span><br><span class="line">320</span><br><span class="line">321</span><br><span class="line">322</span><br><span class="line">323</span><br><span class="line">324</span><br><span class="line">325</span><br><span class="line">326</span><br><span class="line">327</span><br><span class="line">328</span><br><span class="line">329</span><br><span class="line">330</span><br><span class="line">331</span><br><span class="line">332</span><br><span class="line">333</span><br><span class="line">334</span><br><span class="line">335</span><br><span class="line">336</span><br><span class="line">337</span><br><span class="line">338</span><br><span class="line">339</span><br><span class="line">340</span><br><span class="line">341</span><br><span class="line">342</span><br><span class="line">343</span><br><span class="line">344</span><br><span class="line">345</span><br><span class="line">346</span><br><span class="line">347</span><br><span class="line">348</span><br><span class="line">349</span><br><span class="line">350</span><br><span class="line">351</span><br><span class="line">352</span><br><span class="line">353</span><br><span class="line">354</span><br><span class="line">355</span><br><span class="line">356</span><br><span class="line">357</span><br><span class="line">358</span><br><span class="line">359</span><br><span class="line">360</span><br><span class="line">361</span><br><span class="line">362</span><br><span class="line">363</span><br><span class="line">364</span><br><span class="line">365</span><br><span class="line">366</span><br><span class="line">367</span><br><span class="line">368</span><br><span class="line">369</span><br><span class="line">370</span><br><span class="line">371</span><br><span class="line">372</span><br><span class="line">373</span><br><span class="line">374</span><br><span class="line">375</span><br><span class="line">376</span><br><span class="line">377</span><br><span class="line">378</span><br><span class="line">379</span><br><span class="line">380</span><br><span class="line">381</span><br><span class="line">382</span><br><span class="line">383</span><br><span class="line">384</span><br><span class="line">385</span><br><span class="line">386</span><br><span class="line">387</span><br><span class="line">388</span><br><span class="line">389</span><br><span class="line">390</span><br><span class="line">391</span><br><span class="line">392</span><br><span class="line">393</span><br><span class="line">394</span><br><span class="line">395</span><br><span class="line">396</span><br><span class="line">397</span><br><span class="line">398</span><br><span class="line">399</span><br><span class="line">400</span><br><span class="line">401</span><br><span class="line">402</span><br><span class="line">403</span><br><span class="line">404</span><br><span class="line">405</span><br><span class="line">406</span><br><span class="line">407</span><br><span class="line">408</span><br><span class="line">409</span><br><span class="line">410</span><br><span class="line">411</span><br><span class="line">412</span><br><span class="line">413</span><br><span class="line">414</span><br><span class="line">415</span><br><span class="line">416</span><br><span class="line">417</span><br><span class="line">418</span><br><span class="line">419</span><br><span class="line">420</span><br><span class="line">421</span><br><span class="line">422</span><br><span class="line">423</span><br><span class="line">424</span><br><span class="line">425</span><br><span class="line">426</span><br><span class="line">427</span><br><span class="line">428</span><br><span class="line">429</span><br><span class="line">430</span><br><span class="line">431</span><br><span class="line">432</span><br><span class="line">433</span><br><span class="line">434</span><br><span class="line">435</span><br><span class="line">436</span><br><span class="line">437</span><br><span class="line">438</span><br><span class="line">439</span><br><span class="line">440</span><br><span class="line">441</span><br><span class="line">442</span><br><span class="line">443</span><br><span class="line">444</span><br><span class="line">445</span><br><span class="line">446</span><br><span class="line">447</span><br><span class="line">448</span><br><span class="line">449</span><br><span class="line">450</span><br><span class="line">451</span><br><span class="line">452</span><br><span class="line">453</span><br><span class="line">454</span><br><span class="line">455</span><br><span class="line">456</span><br><span class="line">457</span><br><span class="line">458</span><br><span class="line">459</span><br><span class="line">460</span><br><span class="line">461</span><br><span class="line">462</span><br><span class="line">463</span><br><span class="line">464</span><br><span class="line">465</span><br><span class="line">466</span><br><span class="line">467</span><br><span class="line">468</span><br><span class="line">469</span><br><span class="line">470</span><br><span class="line">471</span><br><span class="line">472</span><br><span class="line">473</span><br><span class="line">474</span><br><span class="line">475</span><br><span class="line">476</span><br><span class="line">477</span><br><span class="line">478</span><br><span class="line">479</span><br><span class="line">480</span><br><span class="line">481</span><br><span class="line">482</span><br><span class="line">483</span><br><span class="line">484</span><br><span class="line">485</span><br><span class="line">486</span><br><span class="line">487</span><br><span class="line">488</span><br><span class="line">489</span><br><span class="line">490</span><br><span class="line">491</span><br><span class="line">492</span><br><span class="line">493</span><br><span class="line">494</span><br><span class="line">495</span><br><span class="line">496</span><br><span class="line">497</span><br><span class="line">498</span><br><span class="line">499</span><br><span class="line">500</span><br><span class="line">501</span><br><span class="line">502</span><br><span class="line">503</span><br><span class="line">504</span><br><span class="line">505</span><br><span class="line">506</span><br><span class="line">507</span><br><span class="line">508</span><br><span class="line">509</span><br><span class="line">510</span><br><span class="line">511</span><br><span class="line">512</span><br><span class="line">513</span><br><span class="line">514</span><br><span class="line">515</span><br><span class="line">516</span><br><span class="line">517</span><br><span class="line">518</span><br><span class="line">519</span><br><span class="line">520</span><br><span class="line">521</span><br><span class="line">522</span><br><span class="line">523</span><br><span class="line">524</span><br><span class="line">525</span><br><span class="line">526</span><br><span class="line">527</span><br><span class="line">528</span><br><span class="line">529</span><br><span class="line">530</span><br><span class="line">531</span><br><span class="line">532</span><br><span class="line">533</span><br><span class="line">534</span><br><span class="line">535</span><br><span class="line">536</span><br><span class="line">537</span><br><span class="line">538</span><br><span class="line">539</span><br><span class="line">540</span><br><span class="line">541</span><br><span class="line">542</span><br><span class="line">543</span><br><span class="line">544</span><br><span class="line">545</span><br><span class="line">546</span><br><span class="line">547</span><br><span class="line">548</span><br><span class="line">549</span><br><span class="line">550</span><br><span class="line">551</span><br><span class="line">552</span><br><span class="line">553</span><br><span class="line">554</span><br><span class="line">555</span><br><span class="line">556</span><br><span class="line">557</span><br><span class="line">558</span><br><span class="line">559</span><br><span class="line">560</span><br><span class="line">561</span><br><span class="line">562</span><br><span class="line">563</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/renderer/components/LockEditor.vue</span></span><br><span class="line">&lt;template&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;security-ide&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="comment">&lt;!-- 1. 顶部状态栏 --&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">header</span> <span class="attr">class</span>=<span class="string">&quot;header&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;controls&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">button</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">class</span>=<span class="string">&quot;btn primary&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          @<span class="attr">click</span>=<span class="string">&quot;handleOpenProject&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">:disabled</span>=<span class="string">&quot;isLocked&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        &gt;</span></span></span><br><span class="line"><span class="language-xml">          🛡️ 打开并锁定工程</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">button</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">class</span>=<span class="string">&quot;btn danger&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          @<span class="attr">click</span>=<span class="string">&quot;handleUnlockProject&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">:disabled</span>=<span class="string">&quot;!isLocked&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        &gt;</span></span></span><br><span class="line"><span class="language-xml">          🔓 解锁并关闭</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;status&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        状态: </span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">span</span> <span class="attr">:class</span>=<span class="string">&quot;[&#x27;badge&#x27;, isLocked ? &#x27;locked&#x27; : &#x27;unlocked&#x27;]&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;&#123; isLocked ? &#x27;ACL 权限独占中&#x27; : &#x27;普通模式&#x27; &#125;&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;path&quot;</span> <span class="attr">v-if</span>=<span class="string">&quot;projectPath&quot;</span>&gt;</span>&#123;&#123; projectPath &#125;&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">header</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">       <span class="comment">&lt;!-- ✨ 一键切换开关 --&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">label</span> <span class="attr">class</span>=<span class="string">&quot;switch-container&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">input</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">type</span>=<span class="string">&quot;checkbox&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">v-model</span>=<span class="string">&quot;isLocked_back&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            @<span class="attr">change</span>=<span class="string">&quot;handleToggleLock&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          /&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;slider&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;label-text&quot;</span>&gt;</span>&#123;&#123; isLocked_back ? &#x27;🛡️ 独占模式&#x27; : &#x27;🔓 普通模式&#x27; &#125;&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    <span class="comment">&lt;!-- 2. 主体内容 --&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">main</span> <span class="attr">class</span>=<span class="string">&quot;workspace&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="comment">&lt;!-- 左侧文件列表 --&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">aside</span> <span class="attr">class</span>=<span class="string">&quot;sidebar&quot;</span> @<span class="attr">contextmenu.prevent</span>=<span class="string">&quot;onSidebarRightClick&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;sidebar-title&quot;</span>&gt;</span>资源管理器<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        </span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-if</span>=<span class="string">&quot;!files.length&quot;</span> <span class="attr">class</span>=<span class="string">&quot;empty-list&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;&#123; isLocked ? &#x27;目录为空&#x27; : &#x27;未打开工程&#x27; &#125;&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">ul</span> <span class="attr">class</span>=<span class="string">&quot;file-list&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">li</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">v-for</span>=<span class="string">&quot;file in files&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">:key</span>=<span class="string">&quot;file.path&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">class</span>=<span class="string">&quot;file-item&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">:class</span>=<span class="string">&quot;&#123; active: currentFile?.path === file.path &#125;&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            @<span class="attr">click</span>=<span class="string">&quot;selectFile(file)&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            @<span class="attr">contextmenu.stop</span>=<span class="string">&quot;onFileRightClick($event, file)&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          &gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;icon&quot;</span>&gt;</span>&#123;&#123; file.type=== &#x27;directory&#x27; ? &#x27;📁&#x27; : &#x27;📄&#x27; &#125;&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;name&quot;</span>&gt;</span>&#123;&#123; file.name &#125;&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">aside</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      <span class="comment">&lt;!-- 右侧编辑器 --&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">section</span> <span class="attr">class</span>=<span class="string">&quot;editor&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-if</span>=<span class="string">&quot;!currentFile&quot;</span> <span class="attr">class</span>=<span class="string">&quot;welcome&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">h3</span>&gt;</span>验证说明<span class="tag">&lt;/<span class="name">h3</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">ol</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">li</span>&gt;</span>点击左上角打开一个测试文件夹。<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">li</span>&gt;</span><span class="tag">&lt;<span class="name">strong</span>&gt;</span>验证锁定<span class="tag">&lt;/<span class="name">strong</span>&gt;</span>：打开系统资源管理器，尝试删除该文件夹下的文件，系统应提示“需要权限”。<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">li</span>&gt;</span><span class="tag">&lt;<span class="name">strong</span>&gt;</span>验证特权<span class="tag">&lt;/<span class="name">strong</span>&gt;</span>：在下方编辑器修改内容，按 Ctrl+S 保存，应成功。<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">li</span>&gt;</span><span class="tag">&lt;<span class="name">strong</span>&gt;</span>验证管理<span class="tag">&lt;/<span class="name">strong</span>&gt;</span>：在左侧列表右键，尝试新建或删除，应成功。<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">ol</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-else</span> <span class="attr">class</span>=<span class="string">&quot;code-wrapper&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;tab-header&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            &#123;&#123; currentFile.name &#125;&#125;</span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">span</span> <span class="attr">v-if</span>=<span class="string">&quot;isDirty&quot;</span> <span class="attr">class</span>=<span class="string">&quot;dot&quot;</span>&gt;</span>●<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">textarea</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">class</span>=<span class="string">&quot;code-area&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            <span class="attr">v-model</span>=<span class="string">&quot;fileContent&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            @<span class="attr">input</span>=<span class="string">&quot;isDirty = true&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            @<span class="attr">keydown.ctrl.s.prevent</span>=<span class="string">&quot;saveFile&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">            @<span class="attr">keydown.meta.s.prevent</span>=<span class="string">&quot;saveFile&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          &gt;</span><span class="tag">&lt;/<span class="name">textarea</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">section</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">main</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    <span class="comment">&lt;!-- 3. 底部日志 --&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">footer</span> <span class="attr">class</span>=<span class="string">&quot;logger&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;log-line&quot;</span> <span class="attr">v-for</span>=<span class="string">&quot;(log, idx) in logs&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;idx&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;time&quot;</span>&gt;</span>[&#123;&#123; log.time &#125;&#125;]<span class="tag">&lt;/<span class="name">span</span>&gt;</span> &#123;&#123; log.msg &#125;&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">footer</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    <span class="comment">&lt;!-- 4. 自定义右键菜单 --&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">div</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">v-if</span>=<span class="string">&quot;contextMenu.visible&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">class</span>=<span class="string">&quot;ctx-menu&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">:style</span>=<span class="string">&quot;&#123; top: contextMenu.y + &#x27;px&#x27;, left: contextMenu.x + &#x27;px&#x27; &#125;&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    &gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;menu-item disabled&quot;</span> <span class="attr">v-if</span>=<span class="string">&quot;contextMenu.target&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        操作: &#123;&#123; contextMenu.target.name &#125;&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;divider&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;menu-item&quot;</span> @<span class="attr">click</span>=<span class="string">&quot;handleCreate(&#x27;file&#x27;)&quot;</span>&gt;</span>✨ 特权新建文件<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;menu-item&quot;</span> @<span class="attr">click</span>=<span class="string">&quot;handleCreate(&#x27;directory&#x27;)&quot;</span>&gt;</span>📁 特权新建文件夹<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;menu-item danger&quot;</span> @<span class="attr">click</span>=<span class="string">&quot;handleDelete&quot;</span> <span class="attr">v-if</span>=<span class="string">&quot;contextMenu.target&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        🗑️ 特权删除</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">    <span class="comment">&lt;!-- ✅ 新增：自定义输入弹窗 --&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-if</span>=<span class="string">&quot;inputBox.visible&quot;</span> <span class="attr">class</span>=<span class="string">&quot;modal-overlay&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;modal-content&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;modal-title&quot;</span>&gt;</span>&#123;&#123; inputBox.title &#125;&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">input</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">id</span>=<span class="string">&quot;custom-input&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">v-model</span>=<span class="string">&quot;inputBox.value&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          @<span class="attr">keydown.enter</span>=<span class="string">&quot;confirmInput&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          @<span class="attr">keydown.esc</span>=<span class="string">&quot;cancelInput&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">class</span>=<span class="string">&quot;modal-input&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> </span></span></span><br><span class="line"><span class="tag"><span class="language-xml">          <span class="attr">autocomplete</span>=<span class="string">&quot;off&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        /&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;modal-actions&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">button</span> <span class="attr">class</span>=<span class="string">&quot;btn&quot;</span> @<span class="attr">click</span>=<span class="string">&quot;cancelInput&quot;</span>&gt;</span>取消<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          <span class="tag">&lt;<span class="name">button</span> <span class="attr">class</span>=<span class="string">&quot;btn primary&quot;</span> @<span class="attr">click</span>=<span class="string">&quot;confirmInput&quot;</span>&gt;</span>确定<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">import &#123; ref, reactive, onBeforeUnmount, onMounted &#125; from &#x27;vue&#x27;</span><br><span class="line"></span><br><span class="line">// --- 类型定义 ---</span><br><span class="line">interface FileNode &#123;</span><br><span class="line">  name: string</span><br><span class="line">  path: string</span><br><span class="line">  type: string</span><br><span class="line">  children?: FileNode[]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">interface Log &#123;</span><br><span class="line">  time: string</span><br><span class="line">  msg: string</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// --- 状态数据 ---</span><br><span class="line">const isLocked = ref(false)</span><br><span class="line">const isLocked_back = ref(false)</span><br><span class="line">const projectPath = ref(&#x27;&#x27;)</span><br><span class="line">const files = ref&lt;FileNode[]&gt;([])</span><br><span class="line">const currentFile = ref&lt;FileNode | null&gt;(null)</span><br><span class="line">const fileContent = ref(&#x27;&#x27;)</span><br><span class="line">const isDirty = ref(false)</span><br><span class="line">const logs = ref&lt;Log[]&gt;([])</span><br><span class="line"></span><br><span class="line">// 右键菜单状态</span><br><span class="line">const contextMenu = reactive(&#123;</span><br><span class="line">  visible: false,</span><br><span class="line">  x: 0,</span><br><span class="line">  y: 0,</span><br><span class="line">  target: null as FileNode | null // null 代表在空白处点击</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">// --- 新增：Input 弹窗的状态 ---</span><br><span class="line">const inputBox = reactive(&#123;</span><br><span class="line">  visible: false,</span><br><span class="line">  title: &#x27;&#x27;,</span><br><span class="line">  value: &#x27;&#x27;,</span><br><span class="line">  resolve: null as ((val: string | null) =&gt; void) | null</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">// --- 新增：封装一个 Promise 化的输入框函数 ---</span><br><span class="line">const showInputBox = (title: string, defaultValue = &#x27;&#x27;) =&gt; &#123;</span><br><span class="line">  inputBox.title = title</span><br><span class="line">  inputBox.value = defaultValue</span><br><span class="line">  inputBox.visible = true</span><br><span class="line">  </span><br><span class="line">  // 自动聚焦输入框 (使用 nextTick)</span><br><span class="line">  setTimeout(() =&gt; &#123;</span><br><span class="line">    document.getElementById(&#x27;custom-input&#x27;)?.focus()</span><br><span class="line">  &#125;, 100)</span><br><span class="line"></span><br><span class="line">  return new Promise&lt;string | null&gt;((resolve) =&gt; &#123;</span><br><span class="line">    inputBox.resolve = resolve</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// --- 新增：弹窗确认与取消 ---</span><br><span class="line">const confirmInput = () =&gt; &#123;</span><br><span class="line">  if (inputBox.resolve) inputBox.resolve(inputBox.value)</span><br><span class="line">  inputBox.visible = false</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const cancelInput = () =&gt; &#123;</span><br><span class="line">  if (inputBox.resolve) inputBox.resolve(null)</span><br><span class="line">  inputBox.visible = false</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// --- 日志工具 ---</span><br><span class="line">const addLog = (msg: string) =&gt; &#123;</span><br><span class="line">  const time = new Date().toLocaleTimeString()</span><br><span class="line">  logs.value.unshift(&#123; time, msg &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">// --- 1. 工程操作 ---</span><br><span class="line"></span><br><span class="line">const handleOpenProject = async () =&gt; &#123;</span><br><span class="line">  try &#123;</span><br><span class="line">    addLog(&#x27;正在请求系统权限锁定...&#x27;)</span><br><span class="line">    // const path = await window.securityAPI.openProject()</span><br><span class="line">      const path = await window.projectAPI.open()</span><br><span class="line">    if (path) &#123;</span><br><span class="line">      projectPath.value = path</span><br><span class="line">      isLocked.value = true</span><br><span class="line">      addLog(`✅ 工程已锁定: $&#123;path&#125;`)</span><br><span class="line">      addLog(&#x27;🔒 此时外部应用无法写入或删除此目录内容&#x27;)</span><br><span class="line">      await refreshFiles()</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">      addLog(&#x27;用户取消了操作&#x27;)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; catch (e: any) &#123;</span><br><span class="line">    addLog(`❌ 锁定失败: $&#123;e.message&#125;`)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 2. ✨ 核心：切换模式</span><br><span class="line">const handleToggleLock = async () =&gt; &#123;</span><br><span class="line">  try &#123;</span><br><span class="line">    console.log(&#x27;正在切换模式...&#x27;, isLocked_back.value)</span><br><span class="line">    // 直接调用统一接口，后端负责上锁或解锁</span><br><span class="line">    await window.projectAPI.toggleLock(isLocked_back.value)</span><br><span class="line">    alert(isLocked_back.value ? &#x27;已开启独占锁定！外部无法修改文件。&#x27; : &#x27;已切换回普通模式。&#x27;)</span><br><span class="line">  &#125; catch (e) &#123;</span><br><span class="line">    console.error(e)</span><br><span class="line">    // 如果切换失败（比如权限不够），回滚开关状态</span><br><span class="line">    isLocked_back.value = !isLocked_back.value</span><br><span class="line">    alert(&#x27;切换失败，请检查日志&#x27;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const handleUnlockProject = async () =&gt; &#123;</span><br><span class="line">  try &#123;</span><br><span class="line">    // await window.securityAPI.closeProject()</span><br><span class="line">    await window.projectAPI.close()</span><br><span class="line">    isLocked.value = false</span><br><span class="line">    projectPath.value = &#x27;&#x27;</span><br><span class="line">    files.value = []</span><br><span class="line">    currentFile.value = null</span><br><span class="line">    addLog(&#x27;🔓 工程已解锁，恢复系统默认权限&#x27;)</span><br><span class="line">  &#125; catch (e: any) &#123;</span><br><span class="line">    addLog(`❌ 解锁失败: $&#123;e.message&#125;`)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const refreshFiles = async () =&gt; &#123;</span><br><span class="line">  if (!projectPath.value) return</span><br><span class="line">  try &#123;</span><br><span class="line">    // 假设后端 ipc 实现了 sec-read-dir</span><br><span class="line">    // 如果没有实现，请在后端补充 fs.readdir 逻辑</span><br><span class="line">    const &#123;tree&#125; = await window.projectAPI.readDir(projectPath.value)</span><br><span class="line">    files.value = tree</span><br><span class="line">    console.log(&#x27;Files:&#x27;, files.value)</span><br><span class="line">  &#125; catch (e) &#123;</span><br><span class="line">    addLog(&#x27;读取文件列表失败 (请确认后端实现了 readDir)&#x27;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// --- 2. 文件操作 ---</span><br><span class="line"></span><br><span class="line">const selectFile = async (file: FileNode) =&gt; &#123;</span><br><span class="line">  if (file.type == &#x27;directory&#x27;) return</span><br><span class="line">  try &#123;</span><br><span class="line">    // 这里为了演示简单，假设你有一个 readFile 的 API</span><br><span class="line">    // 或者我们直接显示一段 Mock 内容，重点验证 Save 功能</span><br><span class="line">    const content = await window.projectAPI.readFile(file.path)</span><br><span class="line">    currentFile.value = file</span><br><span class="line">    // fileContent.value = content</span><br><span class="line">    fileContent.value = `// 这是文件 $&#123;file.name&#125;\n// 它是只读的 (对于外部程序)\n// 试着修改并按 Ctrl+S 保存。\n// $&#123;content&#125;`</span><br><span class="line">    isDirty.value = false</span><br><span class="line">    addLog(`已打开: $&#123;file.name&#125;`)</span><br><span class="line">  &#125; catch (e) &#123;</span><br><span class="line">    addLog(&#x27;读取文件内容失败&#x27;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const saveFile = async () =&gt; &#123;</span><br><span class="line">  if (!currentFile.value) return</span><br><span class="line">  addLog(`正在通过特权通道保存 $&#123;currentFile.value.name&#125;...`)</span><br><span class="line">  </span><br><span class="line">  try &#123;</span><br><span class="line">    // ✨ 核心验证点：特权写入</span><br><span class="line">    // await window.securityAPI.writeFile(currentFile.value.path, fileContent.value)</span><br><span class="line">      await window.projectAPI.writeFile(currentFile.value.path, fileContent.value)</span><br><span class="line">    isDirty.value = false</span><br><span class="line">    addLog(`✅ 保存成功！(ACL 权限临时提权完成)`)</span><br><span class="line">  &#125; catch (e: any) &#123;</span><br><span class="line">    addLog(`❌ 保存失败: $&#123;e.message&#125;`)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// --- 3. 右键菜单与新建/删除 ---</span><br><span class="line"></span><br><span class="line">const onSidebarRightClick = (e: MouseEvent) =&gt; &#123;</span><br><span class="line">  showMenu(e.clientX, e.clientY, null)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const onFileRightClick = (e: MouseEvent, file: FileNode) =&gt; &#123;</span><br><span class="line">  showMenu(e.clientX, e.clientY, file)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const showMenu = (x: number, y: number, target: FileNode | null) =&gt; &#123;</span><br><span class="line">  contextMenu.visible = true</span><br><span class="line">  contextMenu.x = x</span><br><span class="line">  contextMenu.y = y</span><br><span class="line">  contextMenu.target = target</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const closeMenu = () =&gt; &#123; contextMenu.visible = false &#125;</span><br><span class="line"></span><br><span class="line">const handleCreate = async (type: &#x27;file&#x27; | &#x27;directory&#x27;) =&gt; &#123;</span><br><span class="line">  closeMenu()</span><br><span class="line">  if (!projectPath.value) return</span><br><span class="line"></span><br><span class="line">//   const name = prompt(`请输入$&#123;type === &#x27;file&#x27; ? &#x27;文件&#x27; : &#x27;文件夹&#x27;&#125;名称:`)</span><br><span class="line">// ✅ 替换为自定义弹窗:</span><br><span class="line">  const name = await showInputBox(`请输入$&#123;type === &#x27;file&#x27; ? &#x27;文件&#x27; : &#x27;文件夹&#x27;&#125;名称:`)</span><br><span class="line">  if (!name) return</span><br><span class="line"></span><br><span class="line">  const targetPath = `$&#123;projectPath.value&#125;/$&#123;name&#125;` // 简单拼接，实际应用请用 path.join</span><br><span class="line">  </span><br><span class="line">  try &#123;</span><br><span class="line">    addLog(`正在创建: $&#123;name&#125;...`)</span><br><span class="line">    // ✨ 核心验证点：特权创建</span><br><span class="line">    await window.projectAPI.create(targetPath, type)</span><br><span class="line">    addLog(`✅ 创建成功`)</span><br><span class="line">    await refreshFiles()</span><br><span class="line">  &#125; catch (e: any) &#123;</span><br><span class="line">    addLog(`❌ 创建失败: $&#123;e.message&#125;`)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const handleDelete = async () =&gt; &#123;</span><br><span class="line">  closeMenu()</span><br><span class="line">  const target = contextMenu.target</span><br><span class="line">  if (!target) return</span><br><span class="line"></span><br><span class="line">  if (!confirm(`确定要强制删除 $&#123;target.name&#125; 吗？`)) return</span><br><span class="line"></span><br><span class="line">  try &#123;</span><br><span class="line">    addLog(`正在删除: $&#123;target.name&#125;...`)</span><br><span class="line">    // ✨ 核心验证点：特权删除</span><br><span class="line">    // await window.securityAPI.delete(target.path)</span><br><span class="line">     // 💥 同样不需要判断模式</span><br><span class="line">    await window.projectAPI.delete(target.path)</span><br><span class="line">    addLog(`✅ 删除成功`)</span><br><span class="line">    if (currentFile.value?.path === target.path) &#123;</span><br><span class="line">      currentFile.value = null</span><br><span class="line">      fileContent.value = &#x27;&#x27;</span><br><span class="line">    &#125;</span><br><span class="line">    await refreshFiles()</span><br><span class="line">  &#125; catch (e: any) &#123;</span><br><span class="line">    addLog(`❌ 删除失败: $&#123;e.message&#125;`)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const handleRename = async (file: any) =&gt; &#123;</span><br><span class="line">  const newName = await showInputBox(&#x27;请输入新名称&#x27;, file.name)</span><br><span class="line">  if (!newName || newName === file.name) return</span><br><span class="line"></span><br><span class="line">  try &#123;</span><br><span class="line">    // ✨ 无论是否锁定，直接调用统一接口</span><br><span class="line">    await window.projectAPI.rename(file.path, newName)</span><br><span class="line">    </span><br><span class="line">    console.log(&#x27;重命名成功&#x27;)</span><br><span class="line">    // 刷新文件列表</span><br><span class="line">    await refreshFiles()</span><br><span class="line">  &#125; catch (e) &#123;</span><br><span class="line">    console.error(&#x27;重命名失败&#x27;, e)</span><br><span class="line">    alert(&#x27;重命名失败：权限不足或文件被占用&#x27;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 全局点击关闭菜单</span><br><span class="line">onMounted(() =&gt; &#123;</span><br><span class="line">  window.addEventListener(&#x27;click&#x27;, closeMenu)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">onBeforeUnmount(() =&gt; &#123;</span><br><span class="line">  window.removeEventListener(&#x27;click&#x27;, closeMenu)</span><br><span class="line">  // 防止开发时刷新页面导致锁死</span><br><span class="line">  if (isLocked.value) &#123;</span><br><span class="line">    window.projectAPI.close()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;style scoped&gt;</span><br><span class="line">.security-ide &#123;</span><br><span class="line">  display: flex;</span><br><span class="line">  flex-direction: column;</span><br><span class="line">  height: 100vh;</span><br><span class="line">  background: #1e1e1e;</span><br><span class="line">  color: #ccc;</span><br><span class="line">  font-family: &#x27;Segoe UI&#x27;, sans-serif;</span><br><span class="line">  user-select: none;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/* Header */</span><br><span class="line">.header &#123;</span><br><span class="line">  height: 50px;</span><br><span class="line">  background: #252526;</span><br><span class="line">  border-bottom: 1px solid #111;</span><br><span class="line">  display: flex;</span><br><span class="line">  align-items: center;</span><br><span class="line">  justify-content: space-between;</span><br><span class="line">  padding: 0 16px;</span><br><span class="line">&#125;</span><br><span class="line">.btn &#123;</span><br><span class="line">  padding: 6px 12px;</span><br><span class="line">  border: none;</span><br><span class="line">  border-radius: 4px;</span><br><span class="line">  cursor: pointer;</span><br><span class="line">  margin-right: 10px;</span><br><span class="line">  font-weight: bold;</span><br><span class="line">  color: white;</span><br><span class="line">  opacity: 0.9;</span><br><span class="line">&#125;</span><br><span class="line">.btn:hover:not(:disabled) &#123; opacity: 1; transform: translateY(-1px); &#125;</span><br><span class="line">.btn:disabled &#123; opacity: 0.3; cursor: not-allowed; &#125;</span><br><span class="line">.primary &#123; background: #0e639c; &#125;</span><br><span class="line">.danger &#123; background: #ab1212; &#125;</span><br><span class="line"></span><br><span class="line">.status &#123; font-size: 13px; display: flex; align-items: center; gap: 8px;&#125;</span><br><span class="line">.badge &#123; padding: 2px 6px; border-radius: 3px; font-size: 12px; color: #111; font-weight: bold;&#125;</span><br><span class="line">.badge.locked &#123; background: #4caf50; &#125;</span><br><span class="line">.badge.unlocked &#123; background: #888; &#125;</span><br><span class="line">.path &#123; color: #888; font-family: monospace; &#125;</span><br><span class="line"></span><br><span class="line">/* Workspace */</span><br><span class="line">.workspace &#123; flex: 1; display: flex; overflow: hidden; &#125;</span><br><span class="line"></span><br><span class="line">.sidebar &#123;</span><br><span class="line">  width: 250px;</span><br><span class="line">  background: #252526;</span><br><span class="line">  border-right: 1px solid #111;</span><br><span class="line">  display: flex;</span><br><span class="line">  flex-direction: column;</span><br><span class="line">&#125;</span><br><span class="line">.sidebar-title &#123;</span><br><span class="line">  padding: 10px 16px; font-size: 11px; font-weight: bold; color: #888; text-transform: uppercase;</span><br><span class="line">&#125;</span><br><span class="line">.empty-list &#123; padding: 20px; text-align: center; color: #555; font-size: 13px; &#125;</span><br><span class="line">.file-list &#123; list-style: none; padding: 0; margin: 0; overflow-y: auto; &#125;</span><br><span class="line">.file-item &#123;</span><br><span class="line">  padding: 4px 16px; cursor: pointer; display: flex; align-items: center; font-size: 13px;</span><br><span class="line">&#125;</span><br><span class="line">.file-item:hover &#123; background: #2a2d2e; &#125;</span><br><span class="line">.file-item.active &#123; background: #37373d; color: white; &#125;</span><br><span class="line">.icon &#123; margin-right: 6px; &#125;</span><br><span class="line"></span><br><span class="line">.editor &#123; flex: 1; display: flex; flex-direction: column; background: #1e1e1e; &#125;</span><br><span class="line">.welcome &#123; </span><br><span class="line">  flex: 1; display: flex; flex-direction: column; </span><br><span class="line">  justify-content: center; align-items: center; color: #666; </span><br><span class="line">  padding: 40px; line-height: 1.8;</span><br><span class="line">&#125;</span><br><span class="line">.welcome ol &#123; text-align: left; &#125;</span><br><span class="line">.code-wrapper &#123; display: flex; flex-direction: column; height: 100%; &#125;</span><br><span class="line">.tab-header &#123; </span><br><span class="line">  background: #2d2d2d; height: 35px; display: flex; align-items: center; </span><br><span class="line">  padding: 0 16px; font-size: 13px; border-top: 1px solid #007fd4; color: white;</span><br><span class="line">&#125;</span><br><span class="line">.dot &#123; margin-left: 8px; font-size: 20px; line-height: 10px; &#125;</span><br><span class="line">.code-area &#123;</span><br><span class="line">  flex: 1; background: #1e1e1e; color: #d4d4d4; border: none; resize: none;</span><br><span class="line">  padding: 16px; font-family: &#x27;Consolas&#x27;, monospace; outline: none; font-size: 14px;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/* Logger */</span><br><span class="line">.logger &#123;</span><br><span class="line">  height: 120px; background: #111; border-top: 1px solid #333;</span><br><span class="line">  padding: 8px; overflow-y: auto; font-family: monospace; font-size: 12px;</span><br><span class="line">&#125;</span><br><span class="line">.log-line &#123; margin-bottom: 4px; color: #ccc; &#125;</span><br><span class="line">.time &#123; color: #569cd6; margin-right: 8px; &#125;</span><br><span class="line"></span><br><span class="line">/* Context Menu */</span><br><span class="line">.ctx-menu &#123;</span><br><span class="line">  position: fixed; background: #252526; border: 1px solid #454545;</span><br><span class="line">  box-shadow: 0 4px 10px rgba(0,0,0,0.5); z-index: 9999;</span><br><span class="line">  min-width: 160px; border-radius: 4px; padding: 4px 0;</span><br><span class="line">&#125;</span><br><span class="line">.menu-item &#123;</span><br><span class="line">  padding: 6px 12px; cursor: pointer; font-size: 13px; color: #ccc;</span><br><span class="line">&#125;</span><br><span class="line">.menu-item:hover:not(.disabled) &#123; background: #094771; color: white; &#125;</span><br><span class="line">.menu-item.disabled &#123; color: #666; cursor: default; border-bottom: 1px solid #333; padding-bottom: 8px; margin-bottom: 4px;&#125;</span><br><span class="line">.menu-item.danger:hover &#123; background: #881111; &#125;</span><br><span class="line">.divider &#123; height: 1px; background: #454545; margin: 4px 0; &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">/* ✅ 新增：弹窗样式 */</span><br><span class="line">.modal-overlay &#123;</span><br><span class="line">  position: fixed;</span><br><span class="line">  top: 0; left: 0; right: 0; bottom: 0;</span><br><span class="line">  background: rgba(0, 0, 0, 0.5);</span><br><span class="line">  display: flex;</span><br><span class="line">  justify-content: center;</span><br><span class="line">  align-items: center;</span><br><span class="line">  z-index: 10000;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.modal-content &#123;</span><br><span class="line">  background: #252526;</span><br><span class="line">  border: 1px solid #454545;</span><br><span class="line">  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);</span><br><span class="line">  width: 300px;</span><br><span class="line">  padding: 16px;</span><br><span class="line">  border-radius: 4px;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.modal-title &#123;</span><br><span class="line">  margin-bottom: 12px;</span><br><span class="line">  font-size: 14px;</span><br><span class="line">  color: #ccc;</span><br><span class="line">  font-weight: bold;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.modal-input &#123;</span><br><span class="line">  width: 100%;</span><br><span class="line">  background: #3c3c3c;</span><br><span class="line">  border: 1px solid #3c3c3c;</span><br><span class="line">  color: white;</span><br><span class="line">  padding: 6px;</span><br><span class="line">  outline: 1px solid transparent;</span><br><span class="line">  margin-bottom: 16px;</span><br><span class="line">  font-family: inherit;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.modal-input:focus &#123;</span><br><span class="line">  outline-color: #0e639c;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.modal-actions &#123;</span><br><span class="line">  display: flex;</span><br><span class="line">  justify-content: flex-end;</span><br><span class="line">  gap: 8px;</span><br><span class="line">&#125;</span><br><span class="line">&lt;/style&gt;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;之前介绍了两种锁定文件的形式，&quot;&gt;&lt;a href=&quot;#之前介绍了两种锁定文件的形式，&quot; class=&quot;headerlink&quot; title=&quot;之前介绍了两种锁定文件的形式，&quot;&gt;&lt;/a&gt;之前介绍了两种锁定文件的形式，&lt;/h2&gt;&lt;p&gt;1、句柄占用&lt;br&gt;2、icacls 权限管理&lt;br&gt;最后我选择了icacls权限管理，因为它更加灵活，可以针对文件夹和文件分别进行权限控制。&lt;/p&gt;
&lt;p&gt;问题，icacls管理 是可行的，但是我担心后面又不想要权限管理了（这个取决于需求）&lt;br&gt;研究后发现改动点其实主要在文件和文件夹的修改 保存 删除 重命名 的方式不一样。&lt;br&gt;icacls 都是先解锁 然后操作 然后上锁&lt;br&gt;正常模式就是没有解锁上锁的过程&lt;br&gt;目前就是要兼容这两种 一键切换。&lt;/p&gt;</summary>
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>Windows (icacls) 这个方案 有文件夹node_modle,.git这种限制吗</title>
    <link href="http://blog.ypintao.com/2025/12/21/%E5%89%8D%E7%AB%AF/electron/%E4%B8%A4%E7%A7%8D%E7%8B%AC%E5%8D%A0%E5%BC%8F%E6%96%B9%E6%A1%88%E6%AF%94%E8%BE%83/"/>
    <id>http://blog.ypintao.com/2025/12/21/%E5%89%8D%E7%AB%AF/electron/%E4%B8%A4%E7%A7%8D%E7%8B%AC%E5%8D%A0%E5%BC%8F%E6%96%B9%E6%A1%88%E6%AF%94%E8%BE%83/</id>
    <published>2025-12-20T17:37:43.041Z</published>
    <updated>2025-12-20T17:39:39.543Z</updated>
    
    <content type="html"><![CDATA[<p><strong>不会崩溃，没有“文件句柄耗尽”的限制，但是有“性能”和“业务逻辑”上的严重副作用。</strong></p><p>我们来对比一下两种方案的区别，你就能明白为什么了：</p><h3 id="1-之前的方案-fs-open-文件句柄"><a href="#1-之前的方案-fs-open-文件句柄" class="headerlink" title="1. 之前的方案 (fs.open &#x2F; 文件句柄)"></a>1. 之前的方案 (fs.open &#x2F; 文件句柄)</h3><ul><li><strong>原理</strong>：程序必须“抓住”每一个文件（占用 1 个句柄）。</li><li><strong>node_modules</strong>：如果有 30,000 个文件，就要抓 30,000 次。</li><li><strong>结果</strong>：<strong>崩溃</strong>。因为操作系统限制单个程序只能抓几千个。</li></ul><h3 id="2-现在的方案-icacls-权限控制"><a href="#2-现在的方案-icacls-权限控制" class="headerlink" title="2. 现在的方案 (icacls &#x2F; 权限控制)"></a>2. 现在的方案 (icacls &#x2F; 权限控制)</h3><ul><li><strong>原理</strong>：程序只是对操作系统下达一个指令：“把这个文件夹及其子文件的属性改为不可写”。</li><li><strong>node_modules</strong>：即使有 30,000 个文件，Electron 也只是发出了<strong>一条</strong>命令。操作系统负责在底层修改元数据（Metadata）。</li><li><strong>结果</strong>：<strong>不会崩溃</strong>。Electron 进程甚至不需要读取这些文件。</li></ul><hr><h3 id="但是！虽然不会崩，我强烈建议你【依然要排除】它们"><a href="#但是！虽然不会崩，我强烈建议你【依然要排除】它们" class="headerlink" title="但是！虽然不会崩，我强烈建议你【依然要排除】它们"></a>但是！虽然不会崩，我强烈建议你【依然要排除】它们</h3><p>原因如下：</p><h4 id="1-性能问题-时间成本"><a href="#1-性能问题-时间成本" class="headerlink" title="1. 性能问题 (时间成本)"></a>1. 性能问题 (时间成本)</h4><p>虽然不会崩，但 <code>icacls</code> 递归修改 3 万个文件的权限需要时间。</p><ul><li><strong>现象</strong>：用户点击“打开工程”，界面可能会<strong>卡住 5~10 秒</strong>（视磁盘速度而定），直到命令执行完。这在用户体验上是不可接受的。</li></ul><h4 id="2-业务逻辑冲突-Git-和-NPM-瘫痪"><a href="#2-业务逻辑冲突-Git-和-NPM-瘫痪" class="headerlink" title="2. 业务逻辑冲突 (Git 和 NPM 瘫痪)"></a>2. 业务逻辑冲突 (Git 和 NPM 瘫痪)</h4><p>这是最致命的：</p><ul><li><strong>如果锁了 <code>.git</code></strong>：你的 Git 就会报错 <code>permission denied</code>，你无法 commit，无法 checkout，Git 状态全乱。</li><li><strong>如果锁了 <code>node_modules</code></strong>：用户想运行 <code>npm install</code> 安装新包时，会直接报错，因为 NPM 无法写入那个文件夹。</li><li><strong>如果锁了 <code>dist</code></strong>：Webpack&#x2F;Vite 无法打包，因为无法清理旧文件。</li></ul><hr><h3 id="解决方案：优化-SecurityManager-以跳过特定目录"><a href="#解决方案：优化-SecurityManager-以跳过特定目录" class="headerlink" title="解决方案：优化 SecurityManager 以跳过特定目录"></a>解决方案：优化 <code>SecurityManager</code> 以跳过特定目录</h3><p><code>icacls</code> 命令本身不支持“排除文件夹”的参数（它要么锁单个，要么 <code>/T</code> 递归锁全部）。</p><p>所以，我们需要在 Node.js 层面做一个**“智能遍历”**：</p><ol><li>读取根目录下的第一层文件&#x2F;文件夹。</li><li>如果是 <code>node_modules</code> 或 <code>.git</code>，<strong>直接跳过</strong>，不执行 icacls。</li><li>如果是其他文件夹（如 <code>src</code>），执行 <strong>递归 (<code>/T</code>)</strong> 锁定。</li><li>如果是文件（如 <code>package.json</code>），执行 <strong>单文件</strong> 锁定。</li></ol><p>下面是优化后的 <code>src/main/security-manager.ts</code> 代码：</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/security-manager.ts</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ... 之前的 import ...</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="variable constant_">IGNORE_DIRS</span> = [<span class="string">&#x27;.git&#x27;</span>, <span class="string">&#x27;node_modules&#x27;</span>, <span class="string">&#x27;dist&#x27;</span>, <span class="string">&#x27;build&#x27;</span>, <span class="string">&#x27;.vscode&#x27;</span>, <span class="string">&#x27;.idea&#x27;</span>]</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">SecurityManager</span> &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">lockProject</span>(<span class="params"><span class="attr">dirPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">rootPath</span> = dirPath</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`🛡️ 正在开启独占模式 (智能排除): <span class="subst">$&#123;dirPath&#125;</span>`</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 智能遍历并锁定</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">applySmartLock</span>(dirPath, <span class="literal">true</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 启动哨兵</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">startSentry</span>(dirPath)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">unlockProject</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">rootPath</span>) <span class="keyword">return</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`🔓 正在解除独占模式...`</span>)</span><br><span class="line">    </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="property">watcher</span>) &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">watcher</span>.<span class="title function_">close</span>()</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">watcher</span> = <span class="literal">null</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">// (解锁 node_modules 也没坏处，反正本来就该是解锁的)</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">applySystemLock</span>(<span class="variable language_">this</span>.<span class="property">rootPath</span>, <span class="literal">false</span>, <span class="literal">true</span>) <span class="comment">// true 表示强制递归</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">rootPath</span> = <span class="string">&#x27;&#x27;</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ... privilegedWrite 等方法保持不变 ...</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><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">applySmartLock</span>(<span class="params"><span class="attr">basePath</span>: <span class="built_in">string</span>, <span class="attr">isLock</span>: <span class="built_in">boolean</span></span>) &#123;</span><br><span class="line">    <span class="comment">// 读取根目录下的第一级内容</span></span><br><span class="line">    <span class="keyword">const</span> items = <span class="keyword">await</span> fs.<span class="title function_">readdir</span>(basePath, &#123; <span class="attr">withFileTypes</span>: <span class="literal">true</span> &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> item <span class="keyword">of</span> items) &#123;</span><br><span class="line">      <span class="keyword">const</span> fullPath = path.<span class="title function_">join</span>(basePath, item.<span class="property">name</span>)</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 1. 检查是否在黑名单中</span></span><br><span class="line">      <span class="keyword">if</span> (<span class="variable constant_">IGNORE_DIRS</span>.<span class="title function_">includes</span>(item.<span class="property">name</span>)) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`⏩ 跳过忽略目录: <span class="subst">$&#123;item.name&#125;</span>`</span>)</span><br><span class="line">        <span class="keyword">continue</span></span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 2. 执行锁定</span></span><br><span class="line">      <span class="keyword">if</span> (item.<span class="title function_">isDirectory</span>()) &#123;</span><br><span class="line">        <span class="comment">// 如果是普通文件夹 (如 src)，执行递归锁定 (/T)</span></span><br><span class="line">        <span class="comment">// 这样 src 下面有 1000 个文件也会被一次性锁住，效率很高</span></span><br><span class="line">        <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">applySystemLock</span>(fullPath, isLock, <span class="literal">true</span>)</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">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(fullPath)</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">// 注意：这里我们只锁根目录的“属性”，不加 /T</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">lockSingleFile</span>(basePath) </span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 修改底层的命令执行方法，增加 recursive 参数</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">applySystemLock</span>(<span class="params"><span class="attr">targetPath</span>: <span class="built_in">string</span>, <span class="attr">isLock</span>: <span class="built_in">boolean</span>, <span class="attr">recursive</span>: <span class="built_in">boolean</span></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">isWindows</span>) &#123;</span><br><span class="line">      <span class="comment">// Windows</span></span><br><span class="line">      <span class="keyword">const</span> perm = isLock </span><br><span class="line">        ? <span class="string">&#x27;/deny Everyone:(OI)(CI)(DE,DC,WD,AD,WEA,WA)&#x27;</span> </span><br><span class="line">        : <span class="string">&#x27;/remove:d Everyone&#x27;</span></span><br><span class="line">      </span><br><span class="line">      <span class="comment">// 关键：只有 recursive 为 true 时才加 /T</span></span><br><span class="line">      <span class="keyword">const</span> recursiveFlag = recursive ? <span class="string">&#x27;/T&#x27;</span> : <span class="string">&#x27;&#x27;</span></span><br><span class="line">      </span><br><span class="line">      <span class="keyword">const</span> cmd = <span class="string">`icacls &quot;<span class="subst">$&#123;targetPath&#125;</span>&quot; <span class="subst">$&#123;perm&#125;</span> <span class="subst">$&#123;recursiveFlag&#125;</span> /C /Q`</span></span><br><span class="line">      <span class="keyword">try</span> &#123; <span class="keyword">await</span> <span class="title function_">execAsync</span>(cmd) &#125; <span class="keyword">catch</span> (e) &#123;&#125;</span><br><span class="line"></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// Mac/Linux</span></span><br><span class="line">      <span class="keyword">const</span> perm = isLock ? <span class="string">&#x27;a-w&#x27;</span> : <span class="string">&#x27;u+w&#x27;</span></span><br><span class="line">      <span class="keyword">const</span> recursiveFlag = recursive ? <span class="string">&#x27;-R&#x27;</span> : <span class="string">&#x27;&#x27;</span></span><br><span class="line">      <span class="keyword">const</span> cmd = <span class="string">`chmod <span class="subst">$&#123;recursiveFlag&#125;</span> <span class="subst">$&#123;perm&#125;</span> &quot;<span class="subst">$&#123;targetPath&#125;</span>&quot;`</span></span><br><span class="line">      <span class="keyword">try</span> &#123; <span class="keyword">await</span> <span class="title function_">execAsync</span>(cmd) &#125; <span class="keyword">catch</span> (e) &#123;&#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">// ... 之前的 lockSingleFile 等 ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><ol><li><strong>技术上</strong>：<code>icacls</code> 方案<strong>没有</strong>文件数量限制，不会崩溃。</li><li><strong>实际上</strong>：为了保证 Git 和 NPM 正常工作，以及打开工程时的速度，<strong>必须排除</strong> <code>node_modules</code> 和 <code>.git</code>。</li><li><strong>实现策略</strong>：<strong>“首层遍历 + 针对性递归”</strong>。即遍历根目录，跳过黑名单，对剩下的文件夹（如 <code>src</code>）进行全量递归锁定。这样既快又安全。</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;strong&gt;不会崩溃，没有“文件句柄耗尽”的限制，但是有“性能”和“业务逻辑”上的严重副作用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们来对比一下两种方案的区别，你就能明白为什么了：&lt;/p&gt;
&lt;h3 id=&quot;1-之前的方案-fs-open-文件句柄&quot;&gt;&lt;a href=&quot;#1</summary>
      
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>独占式文件锁定_icacls</title>
    <link href="http://blog.ypintao.com/2025/12/21/%E5%89%8D%E7%AB%AF/electron/%E7%8B%AC%E5%8D%A0%E5%BC%8F%E5%B7%A5%E5%85%B7%E8%A7%A3%E9%94%81%E6%96%87%E4%BB%B6/"/>
    <id>http://blog.ypintao.com/2025/12/21/%E5%89%8D%E7%AB%AF/electron/%E7%8B%AC%E5%8D%A0%E5%BC%8F%E5%B7%A5%E5%85%B7%E8%A7%A3%E9%94%81%E6%96%87%E4%BB%B6/</id>
    <published>2025-12-20T17:32:16.024Z</published>
    <updated>2025-12-20T17:35:07.018Z</updated>
    
    <content type="html"><![CDATA[<p>这是一个非常必要的工具脚本。因为一旦开发过程中 App 崩溃或者调试中断，文件夹就会保持在“拒绝写入”的状态，连你自己都删不掉它。</p><p>这个脚本作为一个独立的 <strong>Node.js 运维脚本</strong>，不依赖 Electron，直接在终端运行即可救急。</p><span id="more"></span><h3 id="🛠️-脚本：reset-lock-js"><a href="#🛠️-脚本：reset-lock-js" class="headerlink" title="🛠️ 脚本：reset-lock.js"></a>🛠️ 脚本：<code>reset-lock.js</code></h3><p>在你的项目根目录下创建一个名为 <code>reset-lock.js</code> 的文件。</p><figure class="highlight javascript"><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></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"> * 用途：当 Electron 应用崩溃导致文件夹被锁死（拒绝访问）时，</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="comment"> * 1. node reset-lock.js &quot;D:/Project/MyLockedFolder&quot;</span></span><br><span class="line"><span class="comment"> * 2. 或者直接运行 node reset-lock.js 然后按提示输入路径</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> &#123; exec &#125; = <span class="built_in">require</span>(<span class="string">&#x27;child_process&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> readline = <span class="built_in">require</span>(<span class="string">&#x27;readline&#x27;</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> isWindows = process.<span class="property">platform</span> === <span class="string">&#x27;win32&#x27;</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> rl = readline.<span class="title function_">createInterface</span>(&#123;</span><br><span class="line">  <span class="attr">input</span>: process.<span class="property">stdin</span>,</span><br><span class="line">  <span class="attr">output</span>: process.<span class="property">stdout</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="keyword">function</span> <span class="title function_">unlockDirectory</span>(<span class="params">targetPath</span>) &#123;</span><br><span class="line">  <span class="comment">// 1. 处理路径格式</span></span><br><span class="line">  <span class="keyword">const</span> absolutePath = path.<span class="title function_">resolve</span>(targetPath.<span class="title function_">trim</span>());</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">if</span> (!fs.<span class="title function_">existsSync</span>(absolutePath)) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`\n❌ 错误：找不到路径 &quot;<span class="subst">$&#123;absolutePath&#125;</span>&quot;`</span>);</span><br><span class="line">    process.<span class="title function_">exit</span>(<span class="number">1</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`\n🛡️  正在尝试解锁: <span class="subst">$&#123;absolutePath&#125;</span>`</span>);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`💻 操作系统: <span class="subst">$&#123;isWindows ? <span class="string">&#x27;Windows&#x27;</span> : <span class="string">&#x27;macOS/Linux&#x27;</span>&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">let</span> cmd = <span class="string">&#x27;&#x27;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (isWindows) &#123;</span><br><span class="line">    <span class="comment">// Windows 解锁命令</span></span><br><span class="line">    <span class="comment">// /remove:d Everyone: 移除“拒绝 Everyone”的规则</span></span><br><span class="line">    <span class="comment">// /T: 递归子文件夹</span></span><br><span class="line">    <span class="comment">// /C: 即使出错继续执行</span></span><br><span class="line">    <span class="comment">// /Q: 静默模式</span></span><br><span class="line">    cmd = <span class="string">`icacls &quot;<span class="subst">$&#123;absolutePath&#125;</span>&quot; /remove:d Everyone /T /C /Q`</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// macOS/Linux 解锁命令</span></span><br><span class="line">    <span class="comment">// u+w: 给当前用户增加写权限</span></span><br><span class="line">    cmd = <span class="string">`chmod -R u+w &quot;<span class="subst">$&#123;absolutePath&#125;</span>&quot;`</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`⚙️  执行命令: <span class="subst">$&#123;cmd&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">exec</span>(cmd, <span class="function">(<span class="params">error, stdout, stderr</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (error) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`\n❌ 解锁失败!`</span>);</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`原因: <span class="subst">$&#123;error.message&#125;</span>`</span>);</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">`提示: 如果文件夹在系统保护目录，请尝试以【管理员身份】运行此脚本。`</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`\n✅ 解锁成功!`</span>);</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`📁 你现在应该可以正常删除或修改 &quot;<span class="subst">$&#123;absolutePath&#125;</span>&quot; 了。`</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    process.<span class="title function_">exit</span>(<span class="number">0</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="comment">// 主流程</span></span><br><span class="line"><span class="keyword">const</span> args = process.<span class="property">argv</span>.<span class="title function_">slice</span>(<span class="number">2</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (args.<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">  <span class="comment">// 如果命令行带了参数：node reset-lock.js ./test</span></span><br><span class="line">  <span class="title function_">unlockDirectory</span>(args[<span class="number">0</span>]);</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">  rl.<span class="title function_">question</span>(<span class="string">&#x27;请输入被锁定的文件夹路径: &#x27;</span>, <span class="function">(<span class="params">answer</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!answer) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;未输入路径，退出。&#x27;</span>);</span><br><span class="line">      process.<span class="title function_">exit</span>(<span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="title function_">unlockDirectory</span>(answer);</span><br><span class="line">    rl.<span class="title function_">close</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h3 id="📖-使用方法"><a href="#📖-使用方法" class="headerlink" title="📖 使用方法"></a>📖 使用方法</h3><h4 id="方式-1：交互式运行（推荐）"><a href="#方式-1：交互式运行（推荐）" class="headerlink" title="方式 1：交互式运行（推荐）"></a>方式 1：交互式运行（推荐）</h4><ol><li>打开终端（在 VS Code 或 CMD 中）。</li><li>运行命令：<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">node reset-lock.js</span><br></pre></td></tr></table></figure></li><li>脚本会提示你输入路径，把被锁的文件夹拖进终端（或者复制路径粘贴进去），回车即可。</li></ol><h4 id="方式-2：命令行参数运行"><a href="#方式-2：命令行参数运行" class="headerlink" title="方式 2：命令行参数运行"></a>方式 2：命令行参数运行</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"><span class="comment"># Windows</span></span><br><span class="line">node reset-lock.js <span class="string">&quot;C:\Users\Admin\Desktop\TestFolder&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Mac</span></span><br><span class="line">node reset-lock.js ./TestFolder</span><br></pre></td></tr></table></figure><hr><h3 id="💡-为什么需要这个脚本？"><a href="#💡-为什么需要这个脚本？" class="headerlink" title="💡 为什么需要这个脚本？"></a>💡 为什么需要这个脚本？</h3><p>你在前面的代码中使用了 <code>icacls ... /deny Everyone</code>。这是一个非常强力的系统级指令。<br>如果你的 Electron App 在开发时：</p><ol><li>热更新（HMR）导致主进程重启，但没来得及触发 <code>will-quit</code>。</li><li>你手动点了 VS Code 的“停止调试”按钮（这属于强制杀进程，不会触发 <code>will-quit</code>）。</li><li>代码报错导致主进程 Crash。</li></ol><p>在这些情况下，<code>unlockProject()</code> <strong>永远不会被执行</strong>。那个文件夹就会像“钉子户”一样留在你的磁盘上，删也删不掉，改也改不了。这时只有运行这个脚本才能“解救”它。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;这是一个非常必要的工具脚本。因为一旦开发过程中 App 崩溃或者调试中断，文件夹就会保持在“拒绝写入”的状态，连你自己都删不掉它。&lt;/p&gt;
&lt;p&gt;这个脚本作为一个独立的 &lt;strong&gt;Node.js 运维脚本&lt;/strong&gt;，不依赖 Electron，直接在终端运行即可救急。&lt;/p&gt;</summary>
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>独占式文件锁定1</title>
    <link href="http://blog.ypintao.com/2025/12/20/%E5%89%8D%E7%AB%AF/electron/%E7%8B%AC%E5%8D%A0%E5%BC%8F%E6%96%87%E4%BB%B6%E9%94%81%E5%AE%9A_%E6%96%87%E4%BB%B6%E5%8F%A5%E6%9F%84%E5%8D%A0%E7%94%A8/"/>
    <id>http://blog.ypintao.com/2025/12/20/%E5%89%8D%E7%AB%AF/electron/%E7%8B%AC%E5%8D%A0%E5%BC%8F%E6%96%87%E4%BB%B6%E9%94%81%E5%AE%9A_%E6%96%87%E4%BB%B6%E5%8F%A5%E6%9F%84%E5%8D%A0%E7%94%A8/</id>
    <published>2025-12-20T15:36:27.385Z</published>
    <updated>2025-12-20T17:31:24.400Z</updated>
    
    <content type="html"><![CDATA[<p>通常被称为**“独占式文件锁定” (Exclusive File Locking)**。<br>不能完全控制 文件夹里面的文件，新建删除都可以</p><span id="more"></span><p>实现这个功能的难点在于：</p><ol><li><strong>操作系统差异</strong>：Windows 的文件锁是强制的（Mandatory），而 Linux&#x2F;macOS 通常是建议性的（Advisory）。</li><li><strong>句柄限制</strong>：操作系统对单个进程能打开的文件句柄数量（File Descriptors）有限制（通常几千个）。如果文件夹里有 <code>node_modules</code> 这种包含数万个文件的目录，直接全锁会导致应用崩溃。</li><li><strong>自身的读写</strong>：锁住后，你自己的应用也需要特殊的写入方式（复用句柄），否则也会被拒绝访问。</li></ol><p>基于 Electron 环境，我为你设计一套基于 <strong>“文件句柄持有 (File Handle Holding)”</strong> + <strong>“Chokidar 监听”</strong> 的完整解决方案。这在 Windows 上效果极佳（完全无法删除&#x2F;重命名&#x2F;修改），在 macOS&#x2F;Linux 上也能阻止大部分操作。</p><hr><h3 id="核心原理"><a href="#核心原理" class="headerlink" title="核心原理"></a>核心原理</h3><ol><li><strong>占用 (Lock)</strong>：主进程使用 <code>fs.open(path, &#39;r+&#39;)</code> 打开文件并<strong>不关闭</strong>，持有该文件的 <code>fd</code> (文件描述符)。在 Windows 上，只要句柄没关，资源管理器就无法删除或重命名该文件。</li><li><strong>自身写入 (Write)</strong>：当你的 IDE 需要保存文件时，<strong>不重新打开文件</strong>，而是直接复用内存中持有的 <code>fd</code> 进行 <code>fs.write</code>。</li><li><strong>自身删除&#x2F;重命名</strong>：先 <code>fs.close(fd)</code> 释放锁，执行操作，如果是重命名则再次锁定新路径。</li></ol><hr><h3 id="1-后端核心逻辑-src-main-lock-manager-ts"><a href="#1-后端核心逻辑-src-main-lock-manager-ts" class="headerlink" title="1. 后端核心逻辑 (src/main/lock-manager.ts)"></a>1. 后端核心逻辑 (<code>src/main/lock-manager.ts</code>)</h3><p>这是整个方案的大脑。建议新建一个类来专门管理锁。</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/lock-manager.ts</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"><span class="keyword">import</span> chokidar <span class="keyword">from</span> <span class="string">&#x27;chokidar&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">LockManager</span> &#123;</span><br><span class="line">  <span class="comment">// 存储路径到文件描述符(fd)的映射</span></span><br><span class="line">  <span class="keyword">private</span> locks = <span class="keyword">new</span> <span class="title class_">Map</span>&lt;<span class="built_in">string</span>, <span class="built_in">number</span>&gt;()</span><br><span class="line">  <span class="keyword">private</span> <span class="attr">watcher</span>: chokidar.<span class="property">FSWatcher</span> | <span class="literal">null</span> = <span class="literal">null</span></span><br><span class="line">  <span class="keyword">private</span> <span class="attr">rootPath</span>: <span class="built_in">string</span> = <span class="string">&#x27;&#x27;</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">async</span> <span class="title function_">lockDirectory</span>(<span class="params"><span class="attr">dirPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="comment">// 1. 先清理之前的锁</span></span><br><span class="line">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">unlockAll</span>()</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">rootPath</span> = dirPath</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`🔒 开始锁定目录: <span class="subst">$&#123;dirPath&#125;</span>`</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 启动监听器，防止外部新建文件“逃逸”</span></span><br><span class="line">    <span class="comment">// (如果有人强行塞入新文件，我们立刻锁定它)</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">watcher</span> = chokidar.<span class="title function_">watch</span>(dirPath, &#123;</span><br><span class="line">      <span class="attr">ignored</span>: <span class="regexp">/(^|[\/\\])\../</span>, <span class="comment">// 忽略隐藏文件</span></span><br><span class="line">      <span class="attr">persistent</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">ignoreInitial</span>: <span class="literal">false</span>, <span class="comment">// 初始扫描也触发 add 事件</span></span><br><span class="line">      <span class="attr">awaitWriteFinish</span>: <span class="literal">true</span> <span class="comment">// 等待写入完成后再锁</span></span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">watcher</span>.<span class="title function_">on</span>(<span class="string">&#x27;add&#x27;</span>, <span class="function">(<span class="params">filePath</span>) =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">lockFile</span>(filePath))</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">watcher</span>.<span class="title function_">on</span>(<span class="string">&#x27;addDir&#x27;</span>, <span class="function">(<span class="params">dirPath</span>) =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">lockFile</span>(dirPath, <span class="literal">true</span>))</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 监听解除锁定（比如被外部强制删除了）</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">watcher</span>.<span class="title function_">on</span>(<span class="string">&#x27;unlink&#x27;</span>, <span class="function">(<span class="params">filePath</span>) =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">releaseLock</span>(filePath))</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">watcher</span>.<span class="title function_">on</span>(<span class="string">&#x27;unlinkDir&#x27;</span>, <span class="function">(<span class="params">dirPath</span>) =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">releaseLock</span>(dirPath))</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">async</span> <span class="title function_">unlockAll</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">watcher</span>) &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">watcher</span>.<span class="title function_">close</span>()</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">watcher</span> = <span class="literal">null</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> [filePath, fd] <span class="keyword">of</span> <span class="variable language_">this</span>.<span class="property">locks</span>.<span class="title function_">entries</span>()) &#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        fs.<span class="title function_">closeSync</span>(fd)</span><br><span class="line">      &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">        <span class="comment">// 忽略关闭错误的句柄</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">locks</span>.<span class="title function_">clear</span>()</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;🔓 已释放所有文件锁&#x27;</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">private</span> <span class="title function_">lockFile</span>(<span class="params"><span class="attr">targetPath</span>: <span class="built_in">string</span>, isDir = <span class="literal">false</span></span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">locks</span>.<span class="title function_">has</span>(targetPath)) <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="comment">// ⚠️ 关键点：使用 &#x27;r+&#x27; (读写模式) 打开并不关闭</span></span><br><span class="line">      <span class="comment">// 对于目录，我们在 Windows 上打开句柄可以防止重命名/移动</span></span><br><span class="line">      <span class="comment">// Linux/Mac 对目录 open 的支持有限，通常只支持 &#x27;r&#x27;</span></span><br><span class="line">      <span class="keyword">const</span> flags = isDir ? <span class="string">&#x27;r&#x27;</span> : <span class="string">&#x27;r+&#x27;</span> </span><br><span class="line">      </span><br><span class="line">      <span class="keyword">const</span> fd = fs.<span class="title function_">openSync</span>(targetPath, flags)</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">locks</span>.<span class="title function_">set</span>(targetPath, fd)</span><br><span class="line">      </span><br><span class="line">      <span class="comment">// console.log(`Locked: $&#123;path.basename(targetPath)&#125;`)</span></span><br><span class="line">    &#125; <span class="keyword">catch</span> (<span class="attr">error</span>: <span class="built_in">any</span>) &#123;</span><br><span class="line">      <span class="comment">// 常见错误：EMFILE (句柄耗尽), EPERM (无权限)</span></span><br><span class="line">      <span class="keyword">if</span> (error.<span class="property">code</span> === <span class="string">&#x27;EMFILE&#x27;</span>) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&#x27;❌ 错误：打开的文件太多，达到了系统上限！建议忽略 node_modules&#x27;</span>)</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">warn</span>(<span class="string">`无法锁定 <span class="subst">$&#123;targetPath&#125;</span>:`</span>, error.<span class="property">message</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="keyword">private</span> <span class="title function_">releaseLock</span>(<span class="params"><span class="attr">targetPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> fd = <span class="variable language_">this</span>.<span class="property">locks</span>.<span class="title function_">get</span>(targetPath)</span><br><span class="line">    <span class="keyword">if</span> (fd !== <span class="literal">undefined</span>) &#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        fs.<span class="title function_">closeSync</span>(fd)</span><br><span class="line">      &#125; <span class="keyword">catch</span> (e) &#123;&#125;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">locks</span>.<span class="title function_">delete</span>(targetPath)</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">//  提供给 IDE 自身的特权操作 API</span></span><br><span class="line">  <span class="comment">// ==========================================</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * 特权写入：复用 fd 进行写入</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">writeLockedFile</span>(<span class="params"><span class="attr">filePath</span>: <span class="built_in">string</span>, <span class="attr">content</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="keyword">let</span> fd = <span class="variable language_">this</span>.<span class="property">locks</span>.<span class="title function_">get</span>(filePath)</span><br><span class="line"></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">if</span> (fd === <span class="literal">undefined</span>) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">lockFile</span>(filePath)</span><br><span class="line">        fd = <span class="variable language_">this</span>.<span class="property">locks</span>.<span class="title function_">get</span>(filePath)</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (fd === <span class="literal">undefined</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;File not locked or accessible&#x27;</span>)</span><br><span class="line"></span><br><span class="line">      <span class="comment">// Node.js 的 fs.writeFile 支持传入 fd ！！！</span></span><br><span class="line">      <span class="comment">// 但直接传 fd 可能会追加内容，所以我们需要先清空文件内容</span></span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">ftruncate</span>(fd, <span class="number">0</span>) <span class="comment">// 清空文件</span></span><br><span class="line">      </span><br><span class="line">      <span class="comment">// 将内容写入 buffer</span></span><br><span class="line">      <span class="keyword">const</span> buffer = <span class="title class_">Buffer</span>.<span class="title function_">from</span>(content, <span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">      </span><br><span class="line">      <span class="comment">// 使用 writeSync 或 write 写入</span></span><br><span class="line">      <span class="comment">// position: 0 (从头开始写)</span></span><br><span class="line">      fs.<span class="title function_">writeSync</span>(fd, buffer, <span class="number">0</span>, buffer.<span class="property">length</span>, <span class="number">0</span>)</span><br><span class="line">      </span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`✅ 特权写入成功: <span class="subst">$&#123;filePath&#125;</span>`</span>)</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">    &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&#x27;写入失败:&#x27;</span>, error)</span><br><span class="line">      <span class="keyword">throw</span> error</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">async</span> <span class="title function_">renameLockedEntry</span>(<span class="params"><span class="attr">oldPath</span>: <span class="built_in">string</span>, <span class="attr">newName</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> dir = path.<span class="title function_">dirname</span>(oldPath)</span><br><span class="line">    <span class="keyword">const</span> newPath = path.<span class="title function_">join</span>(dir, newName)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 必须先释放锁，否则自己也无法重命名 (Windows特性)</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">releaseLock</span>(oldPath)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">rename</span>(oldPath, newPath)</span><br><span class="line">      <span class="comment">// 2. 这里的锁会由 Chokidar 的 &#x27;add&#x27; 事件自动加上，或者手动加</span></span><br><span class="line">      <span class="comment">// this.lockFile(newPath) </span></span><br><span class="line">      <span class="keyword">return</span> newPath</span><br><span class="line">    &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">      <span class="comment">// 失败了尝试重新锁回去</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">lockFile</span>(oldPath)</span><br><span class="line">      <span class="keyword">throw</span> error</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">async</span> <span class="title function_">deleteLockedEntry</span>(<span class="params"><span class="attr">targetPath</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="comment">// 1. 释放锁</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">releaseLock</span>(targetPath)</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 如果是目录，可能需要递归释放子文件锁</span></span><br><span class="line">    <span class="comment">// 简单起见，这里假设外部调用逻辑会处理，或者依赖 Chokidar 的 unlink</span></span><br><span class="line">    <span class="comment">// 为了稳健，我们遍历一下 map 释放所有子路径</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">const</span> key <span class="keyword">of</span> <span class="variable language_">this</span>.<span class="property">locks</span>.<span class="title function_">keys</span>()) &#123;</span><br><span class="line">      <span class="keyword">if</span> (key.<span class="title function_">startsWith</span>(targetPath)) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">releaseLock</span>(key)</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">try</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">remove</span>(targetPath)</span><br><span class="line">    &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">      <span class="comment">// 恢复锁很难，因为可能删了一半</span></span><br><span class="line">      <span class="keyword">throw</span> error</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">async</span> <span class="title function_">createLockedFile</span>(<span class="params"><span class="attr">folderPath</span>: <span class="built_in">string</span>, <span class="attr">name</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> fullPath = path.<span class="title function_">join</span>(folderPath, name)</span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">ensureFile</span>(fullPath)</span><br><span class="line">      <span class="comment">// 创建完立刻锁</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="title function_">lockFile</span>(fullPath)</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="keyword">export</span> <span class="keyword">const</span> lockManager = <span class="keyword">new</span> <span class="title class_">LockManager</span>()</span><br></pre></td></tr></table></figure><hr><h3 id="2-注册-IPC-接口-src-main-ipc-lock-ipc-ts"><a href="#2-注册-IPC-接口-src-main-ipc-lock-ipc-ts" class="headerlink" title="2. 注册 IPC 接口 (src/main/ipc/lock-ipc.ts)"></a>2. 注册 IPC 接口 (<code>src/main/ipc/lock-ipc.ts</code>)</h3><p>将这些特权操作暴露给前端。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/ipc/lock-ipc.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; ipcMain &#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; lockManager &#125; <span class="keyword">from</span> <span class="string">&#x27;../lock-manager&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">setupLockIPC</span>(<span class="params"></span>) &#123;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 1. 打开并锁定工程</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;lock-open-project&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">dirPath</span>: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> lockManager.<span class="title function_">lockDirectory</span>(dirPath)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 关闭工程（释放锁）</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;lock-close-project&#x27;</span>, <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> lockManager.<span class="title function_">unlockAll</span>()</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3. 特权写入 (替代 fs.writeFile)</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;lock-write-file&#x27;</span>, <span class="title function_">async</span> (_, &#123; filePath, content &#125;) =&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> lockManager.<span class="title function_">writeLockedFile</span>(filePath, content)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 4. 特权删除</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;lock-delete&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">filePath</span>: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> lockManager.<span class="title function_">deleteLockedEntry</span>(filePath)</span><br><span class="line">  &#125;)</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 5. 特权重命名</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;lock-rename&#x27;</span>, <span class="title function_">async</span> (_, &#123; oldPath, newName &#125;) =&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> lockManager.<span class="title function_">renameLockedEntry</span>(oldPath, newName)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>并在 <code>src/main/index.ts</code> 中调用：</strong></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; setupLockIPC &#125; <span class="keyword">from</span> <span class="string">&#x27;./ipc/lock-ipc&#x27;</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="title function_">setupLockIPC</span>()</span><br></pre></td></tr></table></figure><hr><h3 id="3-Preload-暴露-src-preload-index-ts"><a href="#3-Preload-暴露-src-preload-index-ts" class="headerlink" title="3. Preload 暴露 (src/preload/index.ts)"></a>3. Preload 暴露 (<code>src/preload/index.ts</code>)</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> lockAPI = &#123;</span><br><span class="line">  <span class="attr">openProject</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;lock-open-project&#x27;</span>, path),</span><br><span class="line">  <span class="attr">closeProject</span>: <span class="function">() =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;lock-close-project&#x27;</span>),</span><br><span class="line">  <span class="attr">writeFile</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span>, <span class="attr">content</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;lock-write-file&#x27;</span>, &#123; <span class="attr">filePath</span>: path, content &#125;),</span><br><span class="line">  <span class="attr">delete</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;lock-delete&#x27;</span>, path),</span><br><span class="line">  <span class="attr">rename</span>: <span class="function">(<span class="params"><span class="attr">oldPath</span>: <span class="built_in">string</span>, <span class="attr">newName</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;lock-rename&#x27;</span>, &#123; oldPath, newName &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// expose ...</span></span><br></pre></td></tr></table></figure><hr><h3 id="4-前端使用示例-Editor-vue"><a href="#4-前端使用示例-Editor-vue" class="headerlink" title="4. 前端使用示例 (Editor.vue)"></a>4. 前端使用示例 (<code>Editor.vue</code>)</h3><p>在前端，你不再使用普通的 <code>fsAPI.saveFile</code>，而是必须使用 <code>lockAPI.writeFile</code>。</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><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">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">// 打开项目时</span><br><span class="line">const openProject = async (path) =&gt; &#123;</span><br><span class="line">  // 这会触发后端递归锁定</span><br><span class="line">  await window.lockAPI.openProject(path)</span><br><span class="line">  console.log(&#x27;项目已打开且已独占锁定&#x27;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 保存文件时</span><br><span class="line">const saveDocument = async (currentFile, content) =&gt; &#123;</span><br><span class="line">  try &#123;</span><br><span class="line">    // 使用特权通道写入</span><br><span class="line">    await window.lockAPI.writeFile(currentFile.path, content)</span><br><span class="line">    console.log(&#x27;保存成功&#x27;)</span><br><span class="line">  &#125; catch (e) &#123;</span><br><span class="line">    console.error(&#x27;保存失败&#x27;, e)</span><br><span class="line">    alert(&#x27;无法保存，可能锁失效或权限不足&#x27;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 退出应用前</span><br><span class="line">window.onbeforeunload = () =&gt; &#123;</span><br><span class="line">  // 最好主动释放，虽然 Electron 进程结束 OS 会自动回收句柄，但主动清理更安全</span><br><span class="line">  window.lockAPI.closeProject()</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><hr><h3 id="⚠️-关键限制与注意事项-必读"><a href="#⚠️-关键限制与注意事项-必读" class="headerlink" title="⚠️ 关键限制与注意事项 (必读)"></a>⚠️ 关键限制与注意事项 (必读)</h3><ol><li><p><strong>文件句柄上限 (EMFILE Error)</strong>：</p><ul><li><strong>现象</strong>：如果用户打开了一个包含 <code>node_modules</code> 的文件夹，里面可能有 30,000+ 个文件。操作系统通常默认限制单进程打开 1024 或 4096 个文件。尝试全锁会直接导致你的应用崩溃或报错。</li><li><strong>解决方案</strong>：<ul><li><strong>忽略策略</strong>：在 <code>LockManager</code> 的 <code>chokidar</code> 配置和递归逻辑中，<strong>必须</strong>忽略 <code>node_modules</code>, <code>.git</code>, <code>dist</code> 等构建目录。只锁定核心代码文件。</li><li><strong>优雅降级</strong>：在 <code>lockFile</code> 方法中捕获 <code>EMFILE</code> 错误，如果句柄满了，停止锁定后续文件，并通知前端“部分文件未锁定”。</li></ul></li></ul></li><li><p><strong>Windows vs POSIX</strong>：</p><ul><li><strong>Windows</strong>：此方案效果极佳。<code>fs.open</code> 默认会阻止删除和重命名。此时如果你尝试用资源管理器删文件，会提示“文件在 Electron 中已打开”。</li><li><strong>macOS &#x2F; Linux</strong>：此方案效果<strong>有限</strong>。Unix 哲学允许删除正在被进程使用的文件（只删除引用，句柄依然有效）。要实现完全禁止修改，通常需要：<ul><li>使用 <code>fs-ext</code> 原生模块调用 <code>flock(fd, LOCK_EX)</code>（排他锁）。但这增加了编译成本。</li><li>或者简单地：在 Unix 上接受“软锁定”，即你的应用能正常跑，但用户如果在终端 <code>rm -rf</code>，你拦不住。</li></ul></li></ul></li><li><p><strong>性能开销</strong>：</p><ul><li>保持数千个文件句柄打开会消耗内存（虽然不多）。</li><li><code>chokidar</code> 监听大量文件会消耗 CPU。</li></ul></li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>这个方案通过 <strong>“不释放句柄 (Leaking FDs intentionally)”</strong> 的方式实现了应用层面的独占。对于开发一个需要“独占模式”的 IDE 来说，这是最标准且纯 JS (Node) 能做到的极限。如果需要更强力的锁（如拦截系统级写入），则需要编写 C++ 驱动层，那将远超 Electron 的范畴。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;通常被称为**“独占式文件锁定” (Exclusive File Locking)**。&lt;br&gt;不能完全控制 文件夹里面的文件，新建删除都可以&lt;/p&gt;</summary>
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>文件菜单显示</title>
    <link href="http://blog.ypintao.com/2025/12/18/%E5%89%8D%E7%AB%AF/electron/%E6%96%87%E4%BB%B6%E8%8F%9C%E5%8D%95%E6%98%BE%E7%A4%BA/"/>
    <id>http://blog.ypintao.com/2025/12/18/%E5%89%8D%E7%AB%AF/electron/%E6%96%87%E4%BB%B6%E8%8F%9C%E5%8D%95%E6%98%BE%E7%A4%BA/</id>
    <published>2025-12-18T15:25:27.246Z</published>
    <updated>2025-12-18T15:25:48.330Z</updated>
    
    <content type="html"><![CDATA[<p>这是一个非常专业的需求。要将一个涉及 Main&#x2F;Preload&#x2F;Renderer 三端的复杂业务封装成一个“开箱即用”的库，我们需要遵循 <strong>“关注点分离”</strong> 和 <strong>“依赖注入”</strong> 的原则。</p><p>为了满足“封装成库”且“参考 VS Code”的要求，我设计了一个名为 <strong><code>electron-tree-explorer</code></strong> 的本地库结构。你可以直接将其视为一个独立的 npm 包来维护。</p><h3 id="📚-库的设计架构"><a href="#📚-库的设计架构" class="headerlink" title="📚 库的设计架构"></a>📚 库的设计架构</h3><p>这个库将包含三个导出入口：</p><ol><li><strong><code>main</code></strong>: 提供给 Electron 主进程注册 IPC 监听器。</li><li><strong><code>preload</code></strong>: 提供给 Preload 脚本暴露 API。</li><li><strong><code>renderer</code></strong>: 提供 Vue 组件 (<code>FileExplorer</code>) 和 CSS。</li></ol><hr><h3 id="🚀-1-核心类型定义-types-ts"><a href="#🚀-1-核心类型定义-types-ts" class="headerlink" title="🚀 1. 核心类型定义 (types.ts)"></a>🚀 1. 核心类型定义 (<code>types.ts</code>)</h3><p>首先定义通信协议，保证前后端类型一致。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/electron-tree-explorer/src/types.ts</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">FileNode</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">path</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;file&#x27;</span> | <span class="string">&#x27;directory&#x27;</span></span><br><span class="line">  <span class="attr">children</span>?: <span class="title class_">FileNode</span>[] <span class="comment">// 如果已加载，则有子节点</span></span><br><span class="line">  <span class="attr">isExpanded</span>?: <span class="built_in">boolean</span>  <span class="comment">// UI状态</span></span><br><span class="line">  <span class="attr">isEditing</span>?: <span class="built_in">boolean</span>   <span class="comment">// 是否正在重命名/新建</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> <span class="title class_">ExplorerEvents</span> = </span><br><span class="line">  | <span class="string">&#x27;ete:read-dir&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;ete:create&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;ete:rename&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;ete:delete&#x27;</span></span><br><span class="line">  | <span class="string">&#x27;ete:open-file&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">CreatePayload</span> &#123;</span><br><span class="line">  <span class="attr">basePath</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;file&#x27;</span> | <span class="string">&#x27;directory&#x27;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h3 id="🏗️-2-主进程逻辑-main-ts"><a href="#🏗️-2-主进程逻辑-main-ts" class="headerlink" title="🏗️ 2. 主进程逻辑 (main.ts)"></a>🏗️ 2. 主进程逻辑 (<code>main.ts</code>)</h3><p>这里封装了所有 <code>fs</code> 操作。为了安全，我们不直接暴露 <code>fs</code>，而是暴露业务方法。</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/electron-tree-explorer/src/main.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; ipcMain, shell &#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">FileNode</span>, <span class="title class_">CreatePayload</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./types&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">FileExplorerServer</span> &#123;</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="title function_">registerHandlers</span>()</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_">registerHandlers</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="comment">// 1. 读取目录 (懒加载模式)</span></span><br><span class="line">    ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;ete:read-dir&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">dirPath</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span>&lt;<span class="title class_">FileNode</span>[]&gt; =&gt; &#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">const</span> names = <span class="keyword">await</span> fs.<span class="title function_">readdir</span>(dirPath)</span><br><span class="line">        <span class="keyword">const</span> <span class="attr">nodes</span>: <span class="title class_">FileNode</span>[] = []</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">const</span> name <span class="keyword">of</span> names) &#123;</span><br><span class="line">          <span class="comment">// 过滤隐藏文件</span></span><br><span class="line">          <span class="keyword">if</span> (name.<span class="title function_">startsWith</span>(<span class="string">&#x27;.&#x27;</span>)) <span class="keyword">continue</span></span><br><span class="line">          </span><br><span class="line">          <span class="keyword">const</span> fullPath = path.<span class="title function_">join</span>(dirPath, name)</span><br><span class="line">          <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">const</span> stats = <span class="keyword">await</span> fs.<span class="title function_">stat</span>(fullPath)</span><br><span class="line">            nodes.<span class="title function_">push</span>(&#123;</span><br><span class="line">              name,</span><br><span class="line">              <span class="attr">path</span>: fullPath,</span><br><span class="line">              <span class="attr">type</span>: stats.<span class="title function_">isDirectory</span>() ? <span class="string">&#x27;directory&#x27;</span> : <span class="string">&#x27;file&#x27;</span>,</span><br><span class="line">              <span class="attr">children</span>: <span class="literal">undefined</span> <span class="comment">// 懒加载，初始为空</span></span><br><span class="line">            &#125;)</span><br><span class="line">          &#125; <span class="keyword">catch</span> (e) &#123; <span class="comment">/* ignore errors */</span> &#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="keyword">return</span> nodes.<span class="title function_">sort</span>(<span class="function">(<span class="params">a, b</span>) =&gt;</span> &#123;</span><br><span class="line">          <span class="keyword">if</span> (a.<span class="property">type</span> === b.<span class="property">type</span>) <span class="keyword">return</span> a.<span class="property">name</span>.<span class="title function_">localeCompare</span>(b.<span class="property">name</span>)</span><br><span class="line">          <span class="keyword">return</span> a.<span class="property">type</span> === <span class="string">&#x27;directory&#x27;</span> ? -<span class="number">1</span> : <span class="number">1</span></span><br><span class="line">        &#125;)</span><br><span class="line">      &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">&#x27;Read dir failed:&#x27;</span>, error)</span><br><span class="line">        <span class="keyword">return</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="comment">// 2. 创建文件/文件夹</span></span><br><span class="line">    ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;ete:create&#x27;</span>, <span class="title function_">async</span> (_, &#123; basePath, name, <span class="keyword">type</span> &#125;: <span class="title class_">CreatePayload</span>) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> targetPath = path.<span class="title function_">join</span>(basePath, name)</span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">await</span> fs.<span class="title function_">pathExists</span>(targetPath)) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Exist&#x27;</span>)</span><br><span class="line">      </span><br><span class="line">      <span class="keyword">if</span> (<span class="keyword">type</span> === <span class="string">&#x27;directory&#x27;</span>) &#123;</span><br><span class="line">        <span class="keyword">await</span> fs.<span class="title function_">ensureDir</span>(targetPath)</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> fs.<span class="title function_">ensureFile</span>(targetPath)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> targetPath</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 重命名</span></span><br><span class="line">    ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;ete:rename&#x27;</span>, <span class="title function_">async</span> (_, &#123; oldPath, newName &#125;) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> dir = path.<span class="title function_">dirname</span>(oldPath)</span><br><span class="line">      <span class="keyword">const</span> newPath = path.<span class="title function_">join</span>(dir, newName)</span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">rename</span>(oldPath, newPath)</span><br><span class="line">      <span class="keyword">return</span> newPath</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 删除 (移入回收站)</span></span><br><span class="line">    ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;ete:delete&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">filePath</span>: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">      <span class="keyword">await</span> shell.<span class="title function_">trashedItem</span>(filePath)</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 5. 打开文件 (使用默认应用)</span></span><br><span class="line">    ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;ete:open-file&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">filePath</span>: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">      <span class="comment">// 如果需要编辑器打开，这里不处理，由前端 emit 事件给主应用</span></span><br><span class="line">      <span class="comment">// 这里只演示用系统默认程序打开</span></span><br><span class="line">      shell.<span class="title function_">openPath</span>(filePath) </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="keyword">export</span> <span class="keyword">function</span> <span class="title function_">registerFileExplorer</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">new</span> <span class="title class_">FileExplorerServer</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h3 id="🌉-3-Preload-桥接-preload-ts"><a href="#🌉-3-Preload-桥接-preload-ts" class="headerlink" title="🌉 3. Preload 桥接 (preload.ts)"></a>🌉 3. Preload 桥接 (<code>preload.ts</code>)</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// packages/electron-tree-explorer/src/preload.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; ipcRenderer, contextBridge &#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">CreatePayload</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./types&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> api = &#123;</span><br><span class="line">  <span class="attr">readDir</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;ete:read-dir&#x27;</span>, path),</span><br><span class="line">  <span class="attr">create</span>: <span class="function">(<span class="params"><span class="attr">payload</span>: <span class="title class_">CreatePayload</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;ete:create&#x27;</span>, payload),</span><br><span class="line">  <span class="attr">rename</span>: <span class="function">(<span class="params"><span class="attr">oldPath</span>: <span class="built_in">string</span>, <span class="attr">newName</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;ete:rename&#x27;</span>, &#123; oldPath, newName &#125;),</span><br><span class="line">  <span class="attr">delete</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;ete:delete&#x27;</span>, path),</span><br><span class="line">  <span class="attr">openFile</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;ete:open-file&#x27;</span>, path)</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="keyword">export</span> <span class="keyword">function</span> <span class="title function_">exposeExplorerAPI</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (process.<span class="property">contextIsolated</span>) &#123;</span><br><span class="line">    contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">&#x27;fileExplorerAPI&#x27;</span>, api)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// @ts-ignore</span></span><br><span class="line">    <span class="variable language_">window</span>.<span class="property">fileExplorerAPI</span> = api</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h3 id="🎨-4-Renderer-组件-Vue"><a href="#🎨-4-Renderer-组件-Vue" class="headerlink" title="🎨 4. Renderer 组件 (Vue)"></a>🎨 4. Renderer 组件 (Vue)</h3><p>这里是逻辑最复杂的地方。为了封装性，我们将 ContextMenu 和 TreeItem 都集成在一起。</p><h4 id="4-1-树节点组件-TreeItem-vue"><a href="#4-1-树节点组件-TreeItem-vue" class="headerlink" title="4.1 树节点组件 (TreeItem.vue)"></a>4.1 树节点组件 (<code>TreeItem.vue</code>)</h4><p>处理递归渲染、重命名输入框、右键菜单事件。</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><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></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;div class=&quot;ete-node&quot;&gt;</span><br><span class="line">    &lt;!-- 节点行 --&gt;</span><br><span class="line">    &lt;div </span><br><span class="line">      class=&quot;ete-label&quot;</span><br><span class="line">      :class=&quot;&#123; &#x27;selected&#x27;: isSelected &#125;&quot;</span><br><span class="line">      :style=&quot;&#123; paddingLeft: `$&#123;depth * 12 + 10&#125;px` &#125;&quot;</span><br><span class="line">      @click=&quot;onLeftClick&quot;</span><br><span class="line">      @contextmenu.prevent=&quot;onRightClick&quot;</span><br><span class="line">    &gt;</span><br><span class="line">      &lt;!-- 箭头 --&gt;</span><br><span class="line">      &lt;span class=&quot;ete-arrow&quot; :class=&quot;&#123; &#x27;open&#x27;: isOpen, &#x27;hidden&#x27;: node.type === &#x27;file&#x27; &#125;&quot;&gt;</span><br><span class="line">        ▶</span><br><span class="line">      &lt;/span&gt;</span><br><span class="line">      </span><br><span class="line">      &lt;!-- 图标 --&gt;</span><br><span class="line">      &lt;span class=&quot;ete-icon&quot;&gt;&#123;&#123; node.type === &#x27;directory&#x27; ? &#x27;📁&#x27; : &#x27;📄&#x27; &#125;&#125;&lt;/span&gt;</span><br><span class="line"></span><br><span class="line">      &lt;!-- 正常名称 --&gt;</span><br><span class="line">      &lt;span v-if=&quot;!isRenaming&quot; class=&quot;ete-name&quot;&gt;&#123;&#123; node.name &#125;&#125;&lt;/span&gt;</span><br><span class="line">      </span><br><span class="line">      &lt;!-- 重命名/新建 输入框 --&gt;</span><br><span class="line">      &lt;input </span><br><span class="line">        v-else</span><br><span class="line">        ref=&quot;inputRef&quot;</span><br><span class="line">        v-model=&quot;editName&quot;</span><br><span class="line">        class=&quot;ete-input&quot;</span><br><span class="line">        @blur=&quot;confirmEdit&quot;</span><br><span class="line">        @keydown.enter=&quot;confirmEdit&quot;</span><br><span class="line">        @keydown.esc=&quot;cancelEdit&quot;</span><br><span class="line">        @click.stop</span><br><span class="line">      /&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line"></span><br><span class="line">    &lt;!-- 子节点递归 --&gt;</span><br><span class="line">    &lt;div v-if=&quot;node.type === &#x27;directory&#x27; &amp;&amp; isOpen&quot;&gt;</span><br><span class="line">      &lt;TreeItem </span><br><span class="line">        v-for=&quot;child in node.children&quot; </span><br><span class="line">        :key=&quot;child.path&quot;</span><br><span class="line">        :node=&quot;child&quot;</span><br><span class="line">        :depth=&quot;depth + 1&quot;</span><br><span class="line">        :selectedPath=&quot;selectedPath&quot;</span><br><span class="line">        @select=&quot;$emit(&#x27;select&#x27;, $event)&quot;</span><br><span class="line">        @context=&quot;$emit(&#x27;context&#x27;, $event)&quot;</span><br><span class="line">        @refresh=&quot;$emit(&#x27;refresh&#x27;, node)&quot; </span><br><span class="line">      /&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">import &#123; ref, nextTick, watch, toRef &#125; from &#x27;vue&#x27;</span><br><span class="line">import &#123; FileNode &#125; from &#x27;./types&#x27;</span><br><span class="line"></span><br><span class="line">const props = defineProps&lt;&#123;</span><br><span class="line">  node: FileNode</span><br><span class="line">  depth: number</span><br><span class="line">  selectedPath: string</span><br><span class="line">&#125;&gt;()</span><br><span class="line"></span><br><span class="line">const emit = defineEmits([&#x27;select&#x27;, &#x27;context&#x27;, &#x27;refresh&#x27;])</span><br><span class="line"></span><br><span class="line">const isOpen = ref(false)</span><br><span class="line">const isRenaming = ref(false)</span><br><span class="line">const editName = ref(&#x27;&#x27;)</span><br><span class="line">const inputRef = ref&lt;HTMLInputElement&gt;()</span><br><span class="line"></span><br><span class="line">// 监听外部控制的编辑状态 (用于新建)</span><br><span class="line">watch(() =&gt; props.node.isEditing, (val) =&gt; &#123;</span><br><span class="line">  if (val) startEdit()</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">const isSelected = toRef(() =&gt; props.selectedPath === props.node.path)</span><br><span class="line"></span><br><span class="line">// 加载子目录</span><br><span class="line">const loadChildren = async () =&gt; &#123;</span><br><span class="line">  if (props.node.children) return // 已加载过</span><br><span class="line">  // @ts-ignore</span><br><span class="line">  const children = await window.fileExplorerAPI.readDir(props.node.path)</span><br><span class="line">  props.node.children = children</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const onLeftClick = async () =&gt; &#123;</span><br><span class="line">  emit(&#x27;select&#x27;, props.node)</span><br><span class="line">  if (props.node.type === &#x27;directory&#x27;) &#123;</span><br><span class="line">    isOpen.value = !isOpen.value</span><br><span class="line">    if (isOpen.value) await loadChildren()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const onRightClick = (e: MouseEvent) =&gt; &#123;</span><br><span class="line">  emit(&#x27;select&#x27;, props.node) // 右键同时也选中</span><br><span class="line">  emit(&#x27;context&#x27;, &#123; event: e, node: props.node &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// === 编辑逻辑 ===</span><br><span class="line">const startEdit = () =&gt; &#123;</span><br><span class="line">  isRenaming.value = true</span><br><span class="line">  editName.value = props.node.name</span><br><span class="line">  nextTick(() =&gt; inputRef.value?.focus())</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const confirmEdit = async () =&gt; &#123;</span><br><span class="line">  if (!isRenaming.value) return</span><br><span class="line">  isRenaming.value = false</span><br><span class="line">  </span><br><span class="line">  if (!editName.value || editName.value === props.node.name) &#123;</span><br><span class="line">    if (props.node.name === &#x27;&#x27;) &#123;</span><br><span class="line">      // 如果是新建文件且名字为空，触发刷新父级以移除临时节点</span><br><span class="line">       emit(&#x27;refresh&#x27;, null) </span><br><span class="line">    &#125;</span><br><span class="line">    return</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  try &#123;</span><br><span class="line">    if (props.node.path.endsWith(&#x27;__temp__&#x27;)) &#123;</span><br><span class="line">      // 新建逻辑</span><br><span class="line">      const basePath = props.node.path.replace(&#x27;__temp__&#x27;, &#x27;&#x27;)</span><br><span class="line">      // @ts-ignore</span><br><span class="line">      await window.fileExplorerAPI.create(&#123;</span><br><span class="line">        basePath,</span><br><span class="line">        name: editName.value,</span><br><span class="line">        type: props.node.type</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">      // 重命名逻辑</span><br><span class="line">      // @ts-ignore</span><br><span class="line">      await window.fileExplorerAPI.rename(props.node.path, editName.value)</span><br><span class="line">    &#125;</span><br><span class="line">    // 成功后请求父级刷新</span><br><span class="line">    emit(&#x27;refresh&#x27;, &#x27;parent&#x27;) </span><br><span class="line">  &#125; catch (e) &#123;</span><br><span class="line">    alert(&#x27;操作失败: &#x27; + e)</span><br><span class="line">    // 失败如果是新建，需要移除</span><br><span class="line">    if (props.node.path.endsWith(&#x27;__temp__&#x27;)) emit(&#x27;refresh&#x27;, null)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const cancelEdit = () =&gt; &#123;</span><br><span class="line">  isRenaming.value = false</span><br><span class="line">  if (props.node.path.endsWith(&#x27;__temp__&#x27;)) emit(&#x27;refresh&#x27;, null)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">defineExpose(&#123; startEdit &#125;)</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;style&gt;</span><br><span class="line">/* 包含 VS Code 风格的基础样式 */</span><br><span class="line">.ete-label &#123;</span><br><span class="line">  display: flex;</span><br><span class="line">  align-items: center;</span><br><span class="line">  cursor: pointer;</span><br><span class="line">  height: 22px;</span><br><span class="line">  color: #cccccc;</span><br><span class="line">  font-size: 13px;</span><br><span class="line">  user-select: none;</span><br><span class="line">  border: 1px solid transparent;</span><br><span class="line">&#125;</span><br><span class="line">.ete-label:hover &#123; background-color: #2a2d2e; &#125;</span><br><span class="line">.ete-label.selected &#123; background-color: #37373d; color: #fff; &#125;</span><br><span class="line"></span><br><span class="line">.ete-arrow &#123;</span><br><span class="line">  font-size: 10px; margin-right: 4px; transition: transform 0.1s;</span><br><span class="line">  color: #999;</span><br><span class="line">  width: 14px; text-align: center;</span><br><span class="line">&#125;</span><br><span class="line">.ete-arrow.open &#123; transform: rotate(90deg); &#125;</span><br><span class="line">.ete-arrow.hidden &#123; visibility: hidden; &#125;</span><br><span class="line"></span><br><span class="line">.ete-icon &#123; margin-right: 6px; &#125;</span><br><span class="line">.ete-input &#123;</span><br><span class="line">  background: #3c3c3c; color: white; border: 1px solid #007fd4;</span><br><span class="line">  outline: none; height: 20px; width: 100%;</span><br><span class="line">&#125;</span><br><span class="line">&lt;/style&gt;</span><br></pre></td></tr></table></figure><h4 id="4-2-主入口组件-FileExplorer-vue"><a href="#4-2-主入口组件-FileExplorer-vue" class="headerlink" title="4.2 主入口组件 (FileExplorer.vue)"></a>4.2 主入口组件 (<code>FileExplorer.vue</code>)</h4><p>负责管理根节点、右键菜单的显示与隐藏。</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><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></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;div class=&quot;ete-container&quot; @click=&quot;selectedPath = &#x27;&#x27;&quot; @contextmenu.prevent=&quot;onRootContext&quot;&gt;</span><br><span class="line">    &lt;!-- 递归树 --&gt;</span><br><span class="line">    &lt;TreeItem </span><br><span class="line">      v-for=&quot;node in tree&quot; </span><br><span class="line">      :key=&quot;node.path&quot;</span><br><span class="line">      ref=&quot;itemRefs&quot;</span><br><span class="line">      :node=&quot;node&quot; </span><br><span class="line">      :depth=&quot;0&quot;</span><br><span class="line">      :selectedPath=&quot;selectedPath&quot;</span><br><span class="line">      @select=&quot;onSelect&quot;</span><br><span class="line">      @context=&quot;onContext&quot;</span><br><span class="line">      @refresh=&quot;refreshNode&quot;</span><br><span class="line">    /&gt;</span><br><span class="line"></span><br><span class="line">    &lt;!-- 右键菜单 --&gt;</span><br><span class="line">    &lt;div v-if=&quot;menu.visible&quot; class=&quot;ete-context-menu&quot; :style=&quot;&#123; top: menu.y + &#x27;px&#x27;, left: menu.x + &#x27;px&#x27; &#125;&quot;&gt;</span><br><span class="line">      &lt;div @click=&quot;handleAction(&#x27;new_file&#x27;)&quot;&gt;新建文件&lt;/div&gt;</span><br><span class="line">      &lt;div @click=&quot;handleAction(&#x27;new_folder&#x27;)&quot;&gt;新建文件夹&lt;/div&gt;</span><br><span class="line">      &lt;div @click=&quot;handleAction(&#x27;rename&#x27;)&quot;&gt;重命名&lt;/div&gt;</span><br><span class="line">      &lt;div class=&quot;ete-divider&quot;&gt;&lt;/div&gt;</span><br><span class="line">      &lt;div @click=&quot;handleAction(&#x27;delete&#x27;)&quot; class=&quot;ete-danger&quot;&gt;删除&lt;/div&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">import &#123; ref, onMounted, reactive, nextTick &#125; from &#x27;vue&#x27;</span><br><span class="line">import TreeItem from &#x27;./TreeItem.vue&#x27;</span><br><span class="line">import &#123; FileNode &#125; from &#x27;./types&#x27;</span><br><span class="line"></span><br><span class="line">const props = defineProps&lt;&#123; rootPath: string &#125;&gt;()</span><br><span class="line">const emit = defineEmits([&#x27;open-file&#x27;])</span><br><span class="line"></span><br><span class="line">const tree = ref&lt;FileNode[]&gt;([])</span><br><span class="line">const selectedPath = ref(&#x27;&#x27;)</span><br><span class="line">const itemRefs = ref([]) // 用于获取子组件实例调用 startEdit</span><br><span class="line"></span><br><span class="line">const menu = reactive(&#123; visible: false, x: 0, y: 0, targetNode: null as FileNode | null &#125;)</span><br><span class="line"></span><br><span class="line">// 初始化</span><br><span class="line">onMounted(async () =&gt; &#123;</span><br><span class="line">  await loadRoot()</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">const loadRoot = async () =&gt; &#123;</span><br><span class="line">  if (!props.rootPath) return</span><br><span class="line">  // @ts-ignore</span><br><span class="line">  tree.value = await window.fileExplorerAPI.readDir(props.rootPath)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const onSelect = (node: FileNode) =&gt; &#123;</span><br><span class="line">  selectedPath.value = node.path</span><br><span class="line">  if (node.type === &#x27;file&#x27;) emit(&#x27;open-file&#x27;, node.path)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const onContext = (&#123; event, node &#125;: any) =&gt; &#123;</span><br><span class="line">  menu.visible = true</span><br><span class="line">  menu.x = event.clientX</span><br><span class="line">  menu.y = event.clientY</span><br><span class="line">  menu.targetNode = node</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const onRootContext = (event: MouseEvent) =&gt; &#123;</span><br><span class="line">  // 空白处右键</span><br><span class="line">  menu.visible = true</span><br><span class="line">  menu.x = event.clientX</span><br><span class="line">  menu.y = event.clientY</span><br><span class="line">  menu.targetNode = null // null 代表根目录</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 刷新逻辑：如果是 &#x27;parent&#x27;，说明子节点有变动，这里简单处理，重新加载整个父节点</span><br><span class="line">// 在生产级库中，应该通过 path 找到父节点并只刷新它</span><br><span class="line">const refreshNode = () =&gt; &#123;</span><br><span class="line">  // 简单实现：全量刷新根目录 (优化点：根据 path 局部刷新)</span><br><span class="line">  loadRoot()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const handleAction = async (action: string) =&gt; &#123;</span><br><span class="line">  menu.visible = false</span><br><span class="line">  const target = menu.targetNode</span><br><span class="line">  </span><br><span class="line">  if (action === &#x27;delete&#x27;) &#123;</span><br><span class="line">    if (!target) return</span><br><span class="line">    if (confirm(`删除 $&#123;target.name&#125;?`)) &#123;</span><br><span class="line">      // @ts-ignore</span><br><span class="line">      await window.fileExplorerAPI.delete(target.path)</span><br><span class="line">      loadRoot()</span><br><span class="line">    &#125;</span><br><span class="line">    return</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  if (action === &#x27;rename&#x27;) &#123;</span><br><span class="line">    if (!target) return</span><br><span class="line">    // 找到对应的组件实例调用 startEdit</span><br><span class="line">    // 实际库中一般使用 bus 或 provide/inject 来控制编辑状态</span><br><span class="line">    target.isEditing = true </span><br><span class="line">    return</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  if (action === &#x27;new_file&#x27; || action === &#x27;new_folder&#x27;) &#123;</span><br><span class="line">    // 1. 确定插入位置（当前文件夹下，或者当前文件同级，或者根目录）</span><br><span class="line">    let insertList = tree.value</span><br><span class="line">    let basePath = props.rootPath</span><br><span class="line">    </span><br><span class="line">    if (target) &#123;</span><br><span class="line">       if (target.type === &#x27;directory&#x27;) &#123;</span><br><span class="line">         // 插入到该文件夹内部（需要先展开）</span><br><span class="line">         if (!target.children) target.children = []</span><br><span class="line">         insertList = target.children</span><br><span class="line">         basePath = target.path</span><br><span class="line">       &#125; else &#123;</span><br><span class="line">         // 插入到同级 (这里简化，暂时只支持根目录或者文件夹内)</span><br><span class="line">         // 实际需要递归查找 parent</span><br><span class="line">       &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    // 2. 插入临时节点</span><br><span class="line">    const tempNode: FileNode = &#123;</span><br><span class="line">      name: &#x27;&#x27;,</span><br><span class="line">      path: basePath + &#x27;/__temp__&#x27;, // 特殊标记</span><br><span class="line">      type: action === &#x27;new_file&#x27; ? &#x27;file&#x27; : &#x27;directory&#x27;,</span><br><span class="line">      isEditing: true</span><br><span class="line">    &#125;</span><br><span class="line">    insertList.unshift(tempNode)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 点击空白关闭菜单</span><br><span class="line">window.addEventListener(&#x27;click&#x27;, () =&gt; &#123; menu.visible = false &#125;)</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;style scoped&gt;</span><br><span class="line">.ete-container &#123;</span><br><span class="line">  height: 100%; overflow-y: auto; background: #252526; color: #ccc;</span><br><span class="line">  font-family: &#x27;Segoe UI&#x27;, sans-serif;</span><br><span class="line">&#125;</span><br><span class="line">.ete-context-menu &#123;</span><br><span class="line">  position: fixed; background: #333; border: 1px solid #454545;</span><br><span class="line">  box-shadow: 0 2px 8px rgba(0,0,0,0.3); z-index: 9999;</span><br><span class="line">  min-width: 120px; padding: 4px 0;</span><br><span class="line">&#125;</span><br><span class="line">.ete-context-menu div &#123;</span><br><span class="line">  padding: 6px 16px; cursor: pointer; font-size: 13px;</span><br><span class="line">&#125;</span><br><span class="line">.ete-context-menu div:hover &#123; background: #094771; color: white; &#125;</span><br><span class="line">.ete-divider &#123; height: 1px; background: #454545; margin: 4px 0; padding: 0 !important; &#125;</span><br><span class="line">.ete-danger:hover &#123; background: #800000 !important; &#125;</span><br><span class="line">&lt;/style&gt;</span><br></pre></td></tr></table></figure><hr><h3 id="📦-5-如何在你的项目中使用"><a href="#📦-5-如何在你的项目中使用" class="headerlink" title="📦 5. 如何在你的项目中使用"></a>📦 5. 如何在你的项目中使用</h3><p>现在你已经有了一个名为 <code>electron-tree-explorer</code> 的本地库（文件夹）。</p><h4 id="步骤-1-注册主进程"><a href="#步骤-1-注册主进程" class="headerlink" title="步骤 1: 注册主进程"></a>步骤 1: 注册主进程</h4><p>在 <code>src/main/index.ts</code> 中：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; registerFileExplorer &#125; <span class="keyword">from</span> <span class="string">&#x27;../packages/electron-tree-explorer/src/main&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 在 app.whenReady() 之后</span></span><br><span class="line"><span class="title function_">registerFileExplorer</span>()</span><br></pre></td></tr></table></figure><h4 id="步骤-2-注册-Preload"><a href="#步骤-2-注册-Preload" class="headerlink" title="步骤 2: 注册 Preload"></a>步骤 2: 注册 Preload</h4><p>在 <code>src/preload/index.ts</code> 中：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; exposeExplorerAPI &#125; <span class="keyword">from</span> <span class="string">&#x27;../packages/electron-tree-explorer/src/preload&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="title function_">exposeExplorerAPI</span>()</span><br></pre></td></tr></table></figure><h4 id="步骤-3-在-Vue-中使用"><a href="#步骤-3-在-Vue-中使用" class="headerlink" title="步骤 3: 在 Vue 中使用"></a>步骤 3: 在 Vue 中使用</h4><p>在你的业务页面 (比如 <code>Sidebar.vue</code>):</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><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">&lt;template&gt;</span><br><span class="line">  &lt;div class=&quot;sidebar&quot;&gt;</span><br><span class="line">    &lt;FileExplorer </span><br><span class="line">      root-path=&quot;D:/MyProject&quot; </span><br><span class="line">      @open-file=&quot;handleOpenFile&quot; </span><br><span class="line">    /&gt;</span><br><span class="line">  &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">import FileExplorer from &#x27;../packages/electron-tree-explorer/src/FileExplorer.vue&#x27;</span><br><span class="line"></span><br><span class="line">const handleOpenFile = (path: string) =&gt; &#123;</span><br><span class="line">  console.log(&#x27;用户请求打开文件:&#x27;, path)</span><br><span class="line">  // 这里调用你自己的 Tab 系统打开文件</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><h3 id="✅-方案亮点"><a href="#✅-方案亮点" class="headerlink" title="✅ 方案亮点"></a>✅ 方案亮点</h3><ol><li><strong>高度封装</strong>：主项目不需要关心 <code>fs.readdir</code> 怎么写，也不需要写 CSS。</li><li><strong>安全</strong>：通过 <code>contextBridge</code> 只暴露了 5 个必要的 API，没有暴露整个 <code>fs</code> 模块。</li><li><strong>VS Code 体验</strong>：<ul><li><strong>懒加载</strong>：只有点开文件夹才读取子目录，性能好。</li><li><strong>内联重命名</strong>：不是弹窗，而是直接在树上变成输入框。</li><li><strong>右键菜单</strong>：自定义 DOM 菜单，样式统一。</li></ul></li><li><strong>可扩展</strong>：如果你想发包，只需要把 <code>packages/electron-tree-explorer</code> 用 <code>tsup</code> 打包，并在 <code>package.json</code> 定义 <code>exports</code> 即可。</li></ol><p>如果你的文件目录结构或者 CSS 样式有特殊要求，直接修改这个 <code>packages</code> 目录下的文件即可，不会污染你的业务代码。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;这是一个非常专业的需求。要将一个涉及 Main&amp;#x2F;Preload&amp;#x2F;Renderer 三端的复杂业务封装成一个“开箱即用”的库，我们需要遵循 &lt;strong&gt;“关注点分离”&lt;/strong&gt; 和 &lt;strong&gt;“依赖注入”&lt;/strong&gt; 的原则。&lt;/p</summary>
      
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>文件比对标红显示</title>
    <link href="http://blog.ypintao.com/2025/12/17/%E5%89%8D%E7%AB%AF/electron/%E6%96%87%E4%BB%B6%E6%AF%94%E5%AF%B9%E6%A0%87%E7%BA%A2%E6%98%BE%E7%A4%BA/"/>
    <id>http://blog.ypintao.com/2025/12/17/%E5%89%8D%E7%AB%AF/electron/%E6%96%87%E4%BB%B6%E6%AF%94%E5%AF%B9%E6%A0%87%E7%BA%A2%E6%98%BE%E7%A4%BA/</id>
    <published>2025-12-17T15:42:43.979Z</published>
    <updated>2025-12-17T15:43:56.808Z</updated>
    
    <content type="html"><![CDATA[<p>既然你已经有了工程文件的结构数据（比如是从 <code>project.config.json</code> 读取出来的树形结构），现在的核心任务是：<strong>验证这些路径在磁盘上是否真的存在</strong>，并根据结果更新 UI。</p><p>这是一个典型的 <strong>“元数据” vs “物理数据”</strong> 的校验场景。</p><p>为了保证性能（避免成百上千次 IPC 通信），我们应该采用 <strong>“批量校验”</strong> 的策略。</p><span id="more"></span><h3 id="1-后端：批量路径校验-src-main-ipc-fs-ipc-ts"><a href="#1-后端：批量路径校验-src-main-ipc-fs-ipc-ts" class="headerlink" title="1. 后端：批量路径校验 (src/main/ipc/fs-ipc.ts)"></a>1. 后端：批量路径校验 (<code>src/main/ipc/fs-ipc.ts</code>)</h3><p>不要一个一个文件去调 <code>fs.existsSync</code>，那样 UI 会卡顿。我们要提供一个接口，前端传一个路径数组，后端返回一个状态 Map。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/ipc/fs-ipc.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; ipcMain &#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">setupFsIPC</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// ... 之前的代码 ...</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// ✅ 新增：批量检查路径是否存在</span></span><br><span class="line">  <span class="comment">// 参数：paths: string[]</span></span><br><span class="line">  <span class="comment">// 返回：Record&lt;string, boolean&gt;  &#123; &quot;/path/to/a&quot;: true, &quot;/path/to/b&quot;: false &#125;</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;fs-check-paths-batch&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">paths</span>: <span class="built_in">string</span>[]) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="attr">result</span>: <span class="title class_">Record</span>&lt;<span class="built_in">string</span>, <span class="built_in">boolean</span>&gt; = &#123;&#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 使用 Promise.all 并行检查，性能最高</span></span><br><span class="line">    <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>(paths.<span class="title function_">map</span>(<span class="title function_">async</span> (p) =&gt; &#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// access 比 exists 更推荐，因为它不会阻塞事件循环</span></span><br><span class="line">        <span class="keyword">await</span> fs.<span class="title function_">access</span>(p)</span><br><span class="line">        result[p] = <span class="literal">true</span></span><br><span class="line">      &#125; <span class="keyword">catch</span> &#123;</span><br><span class="line">        result[p] = <span class="literal">false</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">return</span> result</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-Preload-暴露-API-src-preload-index-ts"><a href="#2-Preload-暴露-API-src-preload-index-ts" class="headerlink" title="2. Preload 暴露 API (src/preload/index.ts)"></a>2. Preload 暴露 API (<code>src/preload/index.ts</code>)</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/preload/index.ts</span></span><br><span class="line"><span class="keyword">const</span> fsAPI = &#123;</span><br><span class="line">  <span class="comment">// ... 之前的 API ...</span></span><br><span class="line">  <span class="attr">checkPathsBatch</span>: <span class="function">(<span class="params"><span class="attr">paths</span>: <span class="built_in">string</span>[]</span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;fs-check-paths-batch&#x27;</span>, paths)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><hr><h3 id="3-前端：递归组件实现-FileTreeItem-vue"><a href="#3-前端：递归组件实现-FileTreeItem-vue" class="headerlink" title="3. 前端：递归组件实现 (FileTreeItem.vue)"></a>3. 前端：递归组件实现 (<code>FileTreeItem.vue</code>)</h3><p>我们需要修改组件，接收一个全局的 <code>fileStatusMap</code>（记录谁存在，谁不存在）。</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><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></pre></td><td class="code"><pre><span class="line">&lt;!-- src/components/explorer/FileTreeItem.vue --&gt;</span><br><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;div class=&quot;tree-node&quot;&gt;</span><br><span class="line">    &lt;div </span><br><span class="line">      class=&quot;item-label&quot;</span><br><span class="line">      :class=&quot;&#123; </span><br><span class="line">        &#x27;selected&#x27;: isSelected,</span><br><span class="line">        &#x27;missing&#x27;: isMissing // 🔴 关键样式：不存在标红</span><br><span class="line">      &#125;&quot;</span><br><span class="line">      :style=&quot;&#123; paddingLeft: `$&#123;depth * 16 + 8&#125;px` &#125;&quot;</span><br><span class="line">      @click.stop=&quot;handleClick&quot;</span><br><span class="line">      @contextmenu.stop=&quot;handleRightClick&quot;</span><br><span class="line">    &gt;</span><br><span class="line">      &lt;!-- 图标：如果丢失显示警告，否则显示正常的文件夹/文件图标 --&gt;</span><br><span class="line">      &lt;span class=&quot;icon&quot;&gt;</span><br><span class="line">        &lt;template v-if=&quot;isMissing&quot;&gt;⚠️&lt;/template&gt;</span><br><span class="line">        &lt;template v-else&gt;</span><br><span class="line">            &#123;&#123; node.type === &#x27;directory&#x27; ? (isOpen ? &#x27;📂&#x27; : &#x27;📁&#x27;) : &#x27;📄&#x27; &#125;&#125;</span><br><span class="line">        &lt;/template&gt;</span><br><span class="line">      &lt;/span&gt;</span><br><span class="line">      </span><br><span class="line">      &lt;span class=&quot;name&quot;&gt;&#123;&#123; node.name &#125;&#125;&lt;/span&gt;</span><br><span class="line">      &lt;span v-if=&quot;isMissing&quot; class=&quot;missing-tag&quot;&gt;(已删除)&lt;/span&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line"></span><br><span class="line">    &lt;!-- 递归渲染 --&gt;</span><br><span class="line">    &lt;div v-if=&quot;node.type === &#x27;directory&#x27; &amp;&amp; isOpen&quot;&gt;</span><br><span class="line">      &lt;FileTreeItem </span><br><span class="line">        v-for=&quot;child in node.children&quot; </span><br><span class="line">        :key=&quot;child.path&quot;</span><br><span class="line">        :node=&quot;child&quot;</span><br><span class="line">        :depth=&quot;depth + 1&quot;</span><br><span class="line">        :selectedPath=&quot;selectedPath&quot;</span><br><span class="line">        :statusMap=&quot;statusMap&quot; </span><br><span class="line">        @select=&quot;$emit(&#x27;select&#x27;, $event)&quot;</span><br><span class="line">      /&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">import &#123; ref, computed &#125; from &#x27;vue&#x27;</span><br><span class="line"></span><br><span class="line">const props = defineProps&lt;&#123;</span><br><span class="line">  node: any</span><br><span class="line">  depth: number</span><br><span class="line">  selectedPath: string</span><br><span class="line">  // 接收父组件传下来的状态表 &#123; &quot;path&quot;: true/false &#125;</span><br><span class="line">  statusMap: Record&lt;string, boolean&gt; </span><br><span class="line">&#125;&gt;()</span><br><span class="line"></span><br><span class="line">const emit = defineEmits([&#x27;select&#x27;])</span><br><span class="line"></span><br><span class="line">const isOpen = ref(false)</span><br><span class="line"></span><br><span class="line">const isSelected = computed(() =&gt; props.selectedPath === props.node.path)</span><br><span class="line"></span><br><span class="line">// ⚠️ 核心判断：如果 map 里显式记为 false，则视为丢失</span><br><span class="line">// (如果 map 里没有这个 key，通常视作还没加载完，暂不标红，或者默认为 true)</span><br><span class="line">const isMissing = computed(() =&gt; &#123;</span><br><span class="line">    return props.statusMap[props.node.path] === false</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">const handleClick = () =&gt; &#123;</span><br><span class="line">  // 🚫 核心逻辑：不存在则禁止点击（或者只能触发特定逻辑）</span><br><span class="line">  if (isMissing.value) &#123;</span><br><span class="line">    console.warn(&#x27;文件不存在，无法打开&#x27;)</span><br><span class="line">    return</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  if (props.node.type === &#x27;directory&#x27;) &#123;</span><br><span class="line">    isOpen.value = !isOpen.value</span><br><span class="line">  &#125;</span><br><span class="line">  emit(&#x27;select&#x27;, props.node)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const handleRightClick = () =&gt; &#123;</span><br><span class="line">    // 即使文件不存在，通常也允许右键，以便用户选择 &quot;从列表中删除&quot;</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;style scoped&gt;</span><br><span class="line">.item-label &#123;</span><br><span class="line">  display: flex;</span><br><span class="line">  align-items: center;</span><br><span class="line">  cursor: pointer;</span><br><span class="line">  padding: 4px 0;</span><br><span class="line">  color: #ccc;</span><br><span class="line">  user-select: none;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/* 正常状态 hover */</span><br><span class="line">.item-label:hover:not(.missing) &#123; background-color: #2a2d2e; &#125;</span><br><span class="line">.item-label.selected &#123; background-color: #37373d; color: white; &#125;</span><br><span class="line"></span><br><span class="line">/* 🔴 丢失文件样式 */</span><br><span class="line">.item-label.missing &#123;</span><br><span class="line">  color: #e51400; /* 鲜艳的红 */</span><br><span class="line">  opacity: 0.8;</span><br><span class="line">  cursor: not-allowed; /* 禁止手势 */</span><br><span class="line">&#125;</span><br><span class="line">.item-label.missing:hover &#123;</span><br><span class="line">  background-color: rgba(229, 20, 0, 0.1); /* 红色背景微光 */</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.icon &#123; margin-right: 6px; width: 16px; text-align: center; &#125;</span><br><span class="line">.missing-tag &#123; font-size: 10px; margin-left: 8px; opacity: 0.7; &#125;</span><br><span class="line">&lt;/style&gt;</span><br></pre></td></tr></table></figure><hr><h3 id="4-前端：父组件逻辑-ProjectExplorer-vue"><a href="#4-前端：父组件逻辑-ProjectExplorer-vue" class="headerlink" title="4. 前端：父组件逻辑 (ProjectExplorer.vue)"></a>4. 前端：父组件逻辑 (<code>ProjectExplorer.vue</code>)</h3><p>在这里我们将树形数据拍平，批量发送给后端校验。</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><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></pre></td><td class="code"><pre><span class="line">&lt;!-- src/components/explorer/ProjectExplorer.vue --&gt;</span><br><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;div class=&quot;explorer&quot;&gt;</span><br><span class="line">    &lt;div class=&quot;toolbar&quot;&gt;</span><br><span class="line">        &lt;button @click=&quot;validateFiles&quot;&gt;🔄 刷新状态&lt;/button&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">    </span><br><span class="line">    &lt;div class=&quot;tree-container&quot;&gt;</span><br><span class="line">      &lt;FileTreeItem </span><br><span class="line">        v-for=&quot;node in projectData&quot; </span><br><span class="line">        :key=&quot;node.path&quot;</span><br><span class="line">        :node=&quot;node&quot;</span><br><span class="line">        :depth=&quot;0&quot;</span><br><span class="line">        :selectedPath=&quot;selectedPath&quot;</span><br><span class="line">        :statusMap=&quot;fileStatusMap&quot; </span><br><span class="line">        @select=&quot;handleSelect&quot;</span><br><span class="line">      /&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">import &#123; ref, onMounted, watch &#125; from &#x27;vue&#x27;</span><br><span class="line">import FileTreeItem from &#x27;./FileTreeItem.vue&#x27;</span><br><span class="line"></span><br><span class="line">// 假设这是你从 project.config.json 读出来的原始数据</span><br><span class="line">const projectData = ref&lt;any[]&gt;([</span><br><span class="line">    // 示例数据</span><br><span class="line">    &#123; </span><br><span class="line">        name: &#x27;src&#x27;, path: &#x27;D:/project/src&#x27;, type: &#x27;directory&#x27;, </span><br><span class="line">        children: [</span><br><span class="line">            &#123; name: &#x27;main.ts&#x27;, path: &#x27;D:/project/src/main.ts&#x27;, type: &#x27;file&#x27; &#125;,</span><br><span class="line">            &#123; name: &#x27;deleted.ts&#x27;, path: &#x27;D:/project/src/deleted.ts&#x27;, type: &#x27;file&#x27; &#125; // 假设这个文件被删了</span><br><span class="line">        ]</span><br><span class="line">    &#125;</span><br><span class="line">])</span><br><span class="line"></span><br><span class="line">const selectedPath = ref(&#x27;&#x27;)</span><br><span class="line">const fileStatusMap = ref&lt;Record&lt;string, boolean&gt;&gt;(&#123;&#125;)</span><br><span class="line"></span><br><span class="line">// === 核心工具：拍平树结构获取所有路径 ===</span><br><span class="line">const getAllPaths = (nodes: any[]): string[] =&gt; &#123;</span><br><span class="line">  let paths: string[] = []</span><br><span class="line">  for (const node of nodes) &#123;</span><br><span class="line">    paths.push(node.path)</span><br><span class="line">    if (node.children) &#123;</span><br><span class="line">      paths = paths.concat(getAllPaths(node.children))</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  return paths</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// === 核心逻辑：批量校验 ===</span><br><span class="line">const validateFiles = async () =&gt; &#123;</span><br><span class="line">  if (projectData.value.length === 0) return</span><br><span class="line"></span><br><span class="line">  // 1. 提取所有路径</span><br><span class="line">  const allPaths = getAllPaths(projectData.value)</span><br><span class="line">  </span><br><span class="line">  // 2. 发送给主进程批量检查</span><br><span class="line">  console.log(&#x27;正在校验文件数量:&#x27;, allPaths.length)</span><br><span class="line">  const result = await window.fsAPI.checkPathsBatch(allPaths)</span><br><span class="line">  </span><br><span class="line">  // 3. 更新状态 Map</span><br><span class="line">  fileStatusMap.value = result</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 初始化时校验一次</span><br><span class="line">onMounted(() =&gt; &#123;</span><br><span class="line">  validateFiles()</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">// 监听数据变化（如果工程文件变了，重新校验）</span><br><span class="line">watch(projectData, () =&gt; &#123;</span><br><span class="line">    validateFiles()</span><br><span class="line">&#125;, &#123; deep: true &#125;)</span><br><span class="line"></span><br><span class="line">const handleSelect = (node: any) =&gt; &#123;</span><br><span class="line">    selectedPath.value = node.path</span><br><span class="line">    // 打开文件逻辑...</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><h3 id="方案总结"><a href="#方案总结" class="headerlink" title="方案总结"></a>方案总结</h3><ol><li><strong>性能优化</strong>：通过 <code>getAllPaths</code> 将树拍平，只需 <strong>一次 IPC 调用</strong> 即可验证整个工程成百上千个文件的状态，不会卡顿。</li><li><strong>状态驱动 UI</strong>：使用 <code>fileStatusMap</code> 传递给所有子组件。Vue 的响应式系统会自动把状态为 <code>false</code> 的节点渲染成红色。</li><li><strong>交互控制</strong>：在子组件内部拦截 <code>handleClick</code>，如果 <code>isMissing</code> 为真，直接 <code>return</code>，实现了“不能点击”。</li></ol><p>这完全符合你的需求。你可以直接把这段样式和逻辑贴进你现有的左侧菜单组件中。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;既然你已经有了工程文件的结构数据（比如是从 &lt;code&gt;project.config.json&lt;/code&gt; 读取出来的树形结构），现在的核心任务是：&lt;strong&gt;验证这些路径在磁盘上是否真的存在&lt;/strong&gt;，并根据结果更新 UI。&lt;/p&gt;
&lt;p&gt;这是一个典型的 &lt;strong&gt;“元数据” vs “物理数据”&lt;/strong&gt; 的校验场景。&lt;/p&gt;
&lt;p&gt;为了保证性能（避免成百上千次 IPC 通信），我们应该采用 &lt;strong&gt;“批量校验”&lt;/strong&gt; 的策略。&lt;/p&gt;</summary>
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>electron-vite IDE左侧读取工程文件</title>
    <link href="http://blog.ypintao.com/2025/12/17/%E5%89%8D%E7%AB%AF/electron/IDE%E5%B7%A6%E4%BE%A7%E8%AF%BB%E5%8F%96%E5%B7%A5%E7%A8%8B%E6%96%87%E4%BB%B6/"/>
    <id>http://blog.ypintao.com/2025/12/17/%E5%89%8D%E7%AB%AF/electron/IDE%E5%B7%A6%E4%BE%A7%E8%AF%BB%E5%8F%96%E5%B7%A5%E7%A8%8B%E6%96%87%E4%BB%B6/</id>
    <published>2025-12-17T15:30:04.028Z</published>
    <updated>2025-12-17T15:43:00.657Z</updated>
    
    <content type="html"><![CDATA[<p>这是一个非常硬核且实用的需求。要实现类似 VS Code &#x2F; Visual Studio 的工程文件资源管理器，我们需要打通 Electron 的 <strong>Main Process (Node.js fs 能力)</strong> 和 <strong>Renderer Process (Vue 组件交互)</strong>。</p><p>由于代码量较大，我将分为 <strong>后端 (IPC&#x2F;Node)</strong> 和 <strong>前端 (Vue 组件)</strong> 两部分来构建。</p><span id="more"></span><h3 id="核心功能规划"><a href="#核心功能规划" class="headerlink" title="核心功能规划"></a>核心功能规划</h3><ol><li><strong>工程元数据</strong>：在根目录维护一个 <code>project.config.json</code>。</li><li><strong>文件树生成</strong>：递归读取文件夹结构，过滤 <code>node_modules</code> 等。</li><li><strong>CRUD 操作</strong>：读取内容、新建文件&#x2F;文件夹、重命名、删除。</li><li><strong>UI 交互</strong>：递归树组件 + 右键菜单。</li></ol><hr><h3 id="第一部分：后端逻辑-Node-js-IPC"><a href="#第一部分：后端逻辑-Node-js-IPC" class="headerlink" title="第一部分：后端逻辑 (Node.js + IPC)"></a>第一部分：后端逻辑 (Node.js + IPC)</h3><p>我们继续沿用模块化的思路，新建 <code>src/main/ipc/fs-ipc.ts</code>。</p><h4 id="1-定义类型与-IPC-逻辑"><a href="#1-定义类型与-IPC-逻辑" class="headerlink" title="1. 定义类型与 IPC 逻辑"></a>1. 定义类型与 IPC 逻辑</h4><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/main/ipc/fs-ipc.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; ipcMain, dialog, <span class="title class_">BrowserWindow</span>, shell &#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span> <span class="comment">// 建议安装 fs-extra: npm install fs-extra @types/fs-extra</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义文件节点类型</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">FileNode</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">path</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">type</span>: <span class="string">&#x27;file&#x27;</span> | <span class="string">&#x27;directory&#x27;</span></span><br><span class="line">  <span class="attr">children</span>?: <span class="title class_">FileNode</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="keyword">const</span> <span class="variable constant_">IGNORE_LIST</span> = [<span class="string">&#x27;.git&#x27;</span>, <span class="string">&#x27;node_modules&#x27;</span>, <span class="string">&#x27;.DS_Store&#x27;</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> buildFileTree = (<span class="attr">dirPath</span>: <span class="built_in">string</span>): <span class="title class_">FileNode</span>[] =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> stats = fs.<span class="title function_">statSync</span>(dirPath)</span><br><span class="line">  <span class="keyword">if</span> (!stats.<span class="title function_">isDirectory</span>()) <span class="keyword">return</span> []</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> children = fs.<span class="title function_">readdirSync</span>(dirPath)</span><br><span class="line">    .<span class="title function_">filter</span>(<span class="function"><span class="params">name</span> =&gt;</span> !<span class="variable constant_">IGNORE_LIST</span>.<span class="title function_">includes</span>(name))</span><br><span class="line">    .<span class="title function_">map</span>(<span class="function"><span class="params">name</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> fullPath = path.<span class="title function_">join</span>(dirPath, name)</span><br><span class="line">      <span class="keyword">const</span> fileStats = fs.<span class="title function_">statSync</span>(fullPath)</span><br><span class="line">      </span><br><span class="line">      <span class="keyword">const</span> <span class="attr">node</span>: <span class="title class_">FileNode</span> = &#123;</span><br><span class="line">        name,</span><br><span class="line">        <span class="attr">path</span>: fullPath,</span><br><span class="line">        <span class="attr">type</span>: fileStats.<span class="title function_">isDirectory</span>() ? <span class="string">&#x27;directory&#x27;</span> : <span class="string">&#x27;file&#x27;</span></span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (node.<span class="property">type</span> === <span class="string">&#x27;directory&#x27;</span>) &#123;</span><br><span class="line">        node.<span class="property">children</span> = <span class="title function_">buildFileTree</span>(fullPath)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> node</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="keyword">return</span> children.<span class="title function_">sort</span>(<span class="function">(<span class="params">a, b</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (a.<span class="property">type</span> === b.<span class="property">type</span>) <span class="keyword">return</span> a.<span class="property">name</span>.<span class="title function_">localeCompare</span>(b.<span class="property">name</span>)</span><br><span class="line">    <span class="keyword">return</span> a.<span class="property">type</span> === <span class="string">&#x27;directory&#x27;</span> ? -<span class="number">1</span> : <span class="number">1</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_">setupFsIPC</span>(<span class="params"></span>) &#123;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 1. 打开工程目录</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;fs-open-project-dialog&#x27;</span>, <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; canceled, filePaths &#125; = <span class="keyword">await</span> dialog.<span class="title function_">showOpenDialog</span>(&#123;</span><br><span class="line">      <span class="attr">properties</span>: [<span class="string">&#x27;openDirectory&#x27;</span>]</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">if</span> (canceled) <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line">    <span class="keyword">return</span> filePaths[<span class="number">0</span>]</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 读取整个工程结构</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;fs-read-project-tree&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">rootPath</span>: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (!fs.<span class="title function_">existsSync</span>(rootPath)) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;Path not found&#x27;</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> configPath = path.<span class="title function_">join</span>(rootPath, <span class="string">&#x27;project.config.json&#x27;</span>)</span><br><span class="line">      <span class="keyword">let</span> projectInfo = &#123; <span class="attr">name</span>: path.<span class="title function_">basename</span>(rootPath), <span class="attr">version</span>: <span class="string">&#x27;1.0.0&#x27;</span> &#125;</span><br><span class="line">      </span><br><span class="line">      <span class="keyword">if</span> (fs.<span class="title function_">existsSync</span>(configPath)) &#123;</span><br><span class="line">        projectInfo = <span class="keyword">await</span> fs.<span class="title function_">readJson</span>(configPath)</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> fs.<span class="title function_">writeJson</span>(configPath, projectInfo, &#123; <span class="attr">spaces</span>: <span class="number">2</span> &#125;)</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> tree = <span class="title function_">buildFileTree</span>(rootPath)</span><br><span class="line">      <span class="keyword">return</span> &#123; tree, projectInfo &#125;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (error) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">error</span>(error)</span><br><span class="line">      <span class="keyword">throw</span> error</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">// 3. 读取文件内容</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;fs-read-file&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">filePath</span>: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> fs.<span class="title function_">readFile</span>(filePath, <span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 4. 保存文件</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;fs-save-file&#x27;</span>, <span class="title function_">async</span> (_, &#123; filePath, content &#125;) =&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> fs.<span class="title function_">writeFile</span>(filePath, content, <span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 5. 创建文件或文件夹</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;fs-create-entry&#x27;</span>, <span class="title function_">async</span> (_, &#123; parentPath, name, <span class="keyword">type</span> &#125;) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> fullPath = path.<span class="title function_">join</span>(parentPath, name)</span><br><span class="line">    <span class="keyword">if</span> (fs.<span class="title function_">existsSync</span>(fullPath)) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;File already exists&#x27;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">type</span> === <span class="string">&#x27;directory&#x27;</span>) &#123;</span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">ensureDir</span>(fullPath)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> fs.<span class="title function_">ensureFile</span>(fullPath)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> fullPath</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 6. 删除</span></span><br><span class="line">  ipcMain.<span class="title function_">handle</span>(<span class="string">&#x27;fs-delete-entry&#x27;</span>, <span class="title function_">async</span> (_, <span class="attr">filePath</span>: <span class="built_in">string</span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 移入回收站 (比直接删除更安全)</span></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> shell.<span class="title function_">trashedItem</span>(filePath)</span><br><span class="line">    <span class="comment">// 或者直接删除: await fs.remove(filePath)</span></span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>记得在 <code>src/main/index.ts</code> 中调用 <code>setupFsIPC()</code>。</strong></p><hr><h3 id="第二部分：Preload-暴露-API"><a href="#第二部分：Preload-暴露-API" class="headerlink" title="第二部分：Preload 暴露 API"></a>第二部分：Preload 暴露 API</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/preload/index.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123; contextBridge, ipcRenderer &#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> fsAPI = &#123;</span><br><span class="line">  <span class="attr">openProject</span>: <span class="function">() =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;fs-open-project-dialog&#x27;</span>),</span><br><span class="line">  <span class="attr">readProjectTree</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;fs-read-project-tree&#x27;</span>, path),</span><br><span class="line">  <span class="attr">readFile</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;fs-read-file&#x27;</span>, path),</span><br><span class="line">  <span class="attr">saveFile</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span>, <span class="attr">content</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;fs-save-file&#x27;</span>, &#123; <span class="attr">filePath</span>: path, content &#125;),</span><br><span class="line">  <span class="attr">createEntry</span>: <span class="function">(<span class="params"><span class="attr">parentPath</span>: <span class="built_in">string</span>, <span class="attr">name</span>: <span class="built_in">string</span>, <span class="attr">type</span>: <span class="string">&#x27;file&#x27;</span> | <span class="string">&#x27;directory&#x27;</span></span>) =&gt;</span> </span><br><span class="line">    ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;fs-create-entry&#x27;</span>, &#123; parentPath, name, <span class="keyword">type</span> &#125;),</span><br><span class="line">  <span class="attr">deleteEntry</span>: <span class="function">(<span class="params"><span class="attr">path</span>: <span class="built_in">string</span></span>) =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">&#x27;fs-delete-entry&#x27;</span>, path)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (process.<span class="property">contextIsolated</span>) &#123;</span><br><span class="line">  contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">&#x27;fsAPI&#x27;</span>, fsAPI)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  <span class="comment">// @ts-ignore</span></span><br><span class="line">  <span class="variable language_">window</span>.<span class="property">fsAPI</span> = fsAPI</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h3 id="第三部分：前端-Vue-组件-递归树-右键菜单"><a href="#第三部分：前端-Vue-组件-递归树-右键菜单" class="headerlink" title="第三部分：前端 Vue 组件 (递归树 + 右键菜单)"></a>第三部分：前端 Vue 组件 (递归树 + 右键菜单)</h3><p>我们将创建两个组件：</p><ol><li><code>FileTreeItem.vue</code>: 递归渲染单个节点。</li><li><code>ProjectExplorer.vue</code>: 容器，处理右键菜单逻辑。</li></ol><h4 id="1-递归子组件-src-components-explorer-FileTreeItem-vue"><a href="#1-递归子组件-src-components-explorer-FileTreeItem-vue" class="headerlink" title="1. 递归子组件 src/components/explorer/FileTreeItem.vue"></a>1. 递归子组件 <code>src/components/explorer/FileTreeItem.vue</code></h4><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></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;div class=&quot;tree-item&quot;&gt;</span><br><span class="line">    &lt;!-- 当前行内容 --&gt;</span><br><span class="line">    &lt;div </span><br><span class="line">      class=&quot;item-label&quot; </span><br><span class="line">      :class=&quot;&#123; selected: isSelected &#125;&quot;</span><br><span class="line">      :style=&quot;&#123; paddingLeft: `$&#123;depth * 16 + 8&#125;px` &#125;&quot;</span><br><span class="line">      @click.stop=&quot;onLeftClick&quot;</span><br><span class="line">      @contextmenu.prevent.stop=&quot;onRightClick&quot;</span><br><span class="line">    &gt;</span><br><span class="line">      &lt;!-- 图标 (可用 iconify 或 svg 替换) --&gt;</span><br><span class="line">      &lt;span class=&quot;icon&quot;&gt;&#123;&#123; node.type === &#x27;directory&#x27; ? (isOpen ? &#x27;📂&#x27; : &#x27;📁&#x27;) : &#x27;📄&#x27; &#125;&#125;&lt;/span&gt;</span><br><span class="line">      &lt;span class=&quot;name&quot;&gt;&#123;&#123; node.name &#125;&#125;&lt;/span&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line"></span><br><span class="line">    &lt;!-- 递归渲染子节点 --&gt;</span><br><span class="line">    &lt;div v-if=&quot;node.type === &#x27;directory&#x27; &amp;&amp; isOpen&quot;&gt;</span><br><span class="line">      &lt;FileTreeItem </span><br><span class="line">        v-for=&quot;child in node.children&quot; </span><br><span class="line">        :key=&quot;child.path&quot;</span><br><span class="line">        :node=&quot;child&quot;</span><br><span class="line">        :depth=&quot;depth + 1&quot;</span><br><span class="line">        :selectedPath=&quot;selectedPath&quot;</span><br><span class="line">        @select=&quot;$emit(&#x27;select&#x27;, $event)&quot;</span><br><span class="line">        @context-menu=&quot;$emit(&#x27;context-menu&#x27;, $event)&quot;</span><br><span class="line">      /&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">import &#123; ref &#125; from &#x27;vue&#x27;</span><br><span class="line"></span><br><span class="line">const props = defineProps&lt;&#123;</span><br><span class="line">  node: any</span><br><span class="line">  depth: number</span><br><span class="line">  selectedPath: string</span><br><span class="line">&#125;&gt;()</span><br><span class="line"></span><br><span class="line">const emit = defineEmits([&#x27;select&#x27;, &#x27;context-menu&#x27;])</span><br><span class="line"></span><br><span class="line">const isOpen = ref(false)</span><br><span class="line"></span><br><span class="line">// 计算是否被选中</span><br><span class="line">const isSelected = computed(() =&gt; props.selectedPath === props.node.path)</span><br><span class="line"></span><br><span class="line">const onLeftClick = () =&gt; &#123;</span><br><span class="line">  if (props.node.type === &#x27;directory&#x27;) &#123;</span><br><span class="line">    isOpen.value = !isOpen.value</span><br><span class="line">  &#125;</span><br><span class="line">  emit(&#x27;select&#x27;, props.node)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const onRightClick = (event: MouseEvent) =&gt; &#123;</span><br><span class="line">  // 将事件和当前节点信息向上抛出</span><br><span class="line">  emit(&#x27;context-menu&#x27;, &#123; event, node: props.node &#125;)</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;style scoped&gt;</span><br><span class="line">.item-label &#123;</span><br><span class="line">  display: flex;</span><br><span class="line">  align-items: center;</span><br><span class="line">  cursor: pointer;</span><br><span class="line">  padding: 4px 0;</span><br><span class="line">  user-select: none;</span><br><span class="line">  color: #ccc;</span><br><span class="line">&#125;</span><br><span class="line">.item-label:hover &#123; background-color: #2a2d2e; &#125;</span><br><span class="line">.item-label.selected &#123; background-color: #37373d; color: white; &#125;</span><br><span class="line">.icon &#123; margin-right: 6px; font-size: 14px; &#125;</span><br><span class="line">.name &#123; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; &#125;</span><br><span class="line">&lt;/style&gt;</span><br></pre></td></tr></table></figure><h4 id="2-主容器组件-src-components-explorer-ProjectExplorer-vue"><a href="#2-主容器组件-src-components-explorer-ProjectExplorer-vue" class="headerlink" title="2. 主容器组件 src/components/explorer/ProjectExplorer.vue"></a>2. 主容器组件 <code>src/components/explorer/ProjectExplorer.vue</code></h4><p>这里包含了<strong>核心业务逻辑</strong>：加载工程、右键菜单、创建&#x2F;删除文件。</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><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></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;div class=&quot;explorer-container&quot;&gt;</span><br><span class="line">    &lt;!-- 顶部工具栏 --&gt;</span><br><span class="line">    &lt;div class=&quot;toolbar&quot;&gt;</span><br><span class="line">      &lt;span&gt;&#123;&#123; projectName || &#x27;未打开工程&#x27; &#125;&#125;&lt;/span&gt;</span><br><span class="line">      &lt;button @click=&quot;openProject&quot; class=&quot;icon-btn&quot;&gt;📂&lt;/button&gt;</span><br><span class="line">      &lt;button @click=&quot;refreshTree&quot; class=&quot;icon-btn&quot; v-if=&quot;projectPath&quot;&gt;🔄&lt;/button&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line"></span><br><span class="line">    &lt;!-- 文件树区域 --&gt;</span><br><span class="line">    &lt;div class=&quot;tree-content&quot; @contextmenu.prevent=&quot;onEmptyAreaRightClick&quot;&gt;</span><br><span class="line">      &lt;FileTreeItem </span><br><span class="line">        v-for=&quot;node in fileTree&quot; </span><br><span class="line">        :key=&quot;node.path&quot;</span><br><span class="line">        :node=&quot;node&quot;</span><br><span class="line">        :depth=&quot;0&quot;</span><br><span class="line">        :selectedPath=&quot;selectedPath&quot;</span><br><span class="line">        @select=&quot;onFileSelect&quot;</span><br><span class="line">        @context-menu=&quot;handleContextMenu&quot;</span><br><span class="line">      /&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line"></span><br><span class="line">    &lt;!-- 自定义右键菜单 --&gt;</span><br><span class="line">    &lt;div </span><br><span class="line">      v-if=&quot;contextMenu.visible&quot; </span><br><span class="line">      class=&quot;context-menu&quot;</span><br><span class="line">      :style=&quot;&#123; top: `$&#123;contextMenu.y&#125;px`, left: `$&#123;contextMenu.x&#125;px` &#125;&quot;</span><br><span class="line">      @blur=&quot;closeContextMenu&quot;</span><br><span class="line">    &gt;</span><br><span class="line">      &lt;div class=&quot;menu-item disabled&quot;&gt;&#123;&#123; contextMenu.targetName &#125;&#125;&lt;/div&gt;</span><br><span class="line">      &lt;div class=&quot;divider&quot;&gt;&lt;/div&gt;</span><br><span class="line">      &lt;div class=&quot;menu-item&quot; @click=&quot;createNew(&#x27;file&#x27;)&quot;&gt;📄 新建文件&lt;/div&gt;</span><br><span class="line">      &lt;div class=&quot;menu-item&quot; @click=&quot;createNew(&#x27;directory&#x27;)&quot;&gt;📁 新建文件夹&lt;/div&gt;</span><br><span class="line">      &lt;div class=&quot;divider&quot;&gt;&lt;/div&gt;</span><br><span class="line">      &lt;div class=&quot;menu-item delete&quot; @click=&quot;deleteItem&quot;&gt;🗑 删除&lt;/div&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">  &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">import &#123; ref, reactive, onMounted, onUnmounted &#125; from &#x27;vue&#x27;</span><br><span class="line">import FileTreeItem from &#x27;./FileTreeItem.vue&#x27;</span><br><span class="line"></span><br><span class="line">// 状态</span><br><span class="line">const projectPath = ref(&#x27;&#x27;)</span><br><span class="line">const projectName = ref(&#x27;&#x27;)</span><br><span class="line">const fileTree = ref&lt;any[]&gt;([])</span><br><span class="line">const selectedPath = ref(&#x27;&#x27;)</span><br><span class="line"></span><br><span class="line">// 右键菜单状态</span><br><span class="line">const contextMenu = reactive(&#123;</span><br><span class="line">  visible: false,</span><br><span class="line">  x: 0,</span><br><span class="line">  y: 0,</span><br><span class="line">  targetNode: null as any | null, // 右键点击的节点</span><br><span class="line">  targetName: &#x27;&#x27;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">// === 1. 打开工程 ===</span><br><span class="line">const openProject = async () =&gt; &#123;</span><br><span class="line">  const path = await window.fsAPI.openProject()</span><br><span class="line">  if (path) &#123;</span><br><span class="line">    projectPath.value = path</span><br><span class="line">    await loadTree()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// === 2. 加载文件树 ===</span><br><span class="line">const loadTree = async () =&gt; &#123;</span><br><span class="line">  if (!projectPath.value) return</span><br><span class="line">  const &#123; tree, projectInfo &#125; = await window.fsAPI.readProjectTree(projectPath.value)</span><br><span class="line">  fileTree.value = tree</span><br><span class="line">  projectName.value = projectInfo.name</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const refreshTree = () =&gt; loadTree()</span><br><span class="line"></span><br><span class="line">// === 3. 文件选择 ===</span><br><span class="line">const onFileSelect = async (node: any) =&gt; &#123;</span><br><span class="line">  selectedPath.value = node.path</span><br><span class="line">  if (node.type === &#x27;file&#x27;) &#123;</span><br><span class="line">    // TODO: 这里可以调用 Dockview 或 EventBus 打开文件编辑器</span><br><span class="line">    // const content = await window.fsAPI.readFile(node.path)</span><br><span class="line">    console.log(&#x27;打开文件:&#x27;, node.path)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// === 4. 右键菜单处理 ===</span><br><span class="line">const handleContextMenu = (&#123; event, node &#125;: any) =&gt; &#123;</span><br><span class="line">  showMenu(event, node)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const onEmptyAreaRightClick = (event: MouseEvent) =&gt; &#123;</span><br><span class="line">  // 在空白处点击，视作在根目录操作</span><br><span class="line">  showMenu(event, &#123; </span><br><span class="line">    path: projectPath.value, </span><br><span class="line">    type: &#x27;directory&#x27;, </span><br><span class="line">    name: &#x27;根目录&#x27; </span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const showMenu = (event: MouseEvent, node: any) =&gt; &#123;</span><br><span class="line">  contextMenu.visible = true</span><br><span class="line">  contextMenu.x = event.clientX</span><br><span class="line">  contextMenu.y = event.clientY</span><br><span class="line">  contextMenu.targetNode = node</span><br><span class="line">  contextMenu.targetName = node.name</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const closeContextMenu = () =&gt; &#123;</span><br><span class="line">  contextMenu.visible = false</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// === 5. 创建文件/文件夹 ===</span><br><span class="line">const createNew = async (type: &#x27;file&#x27; | &#x27;directory&#x27;) =&gt; &#123;</span><br><span class="line">  closeContextMenu()</span><br><span class="line">  if (!contextMenu.targetNode) return</span><br><span class="line"></span><br><span class="line">  // 确定父目录 (如果是文件，则在同级创建；如果是文件夹，则在内部创建)</span><br><span class="line">  const parentPath = contextMenu.targetNode.type === &#x27;directory&#x27; </span><br><span class="line">    ? contextMenu.targetNode.path </span><br><span class="line">    : contextMenu.targetNode.path.replace(/[\\/][^\\/]+$/, &#x27;&#x27;) // 获取父级路径</span><br><span class="line">  </span><br><span class="line">  // 简单起见，这里用 prompt，实际可以用 Modal 弹窗</span><br><span class="line">  const name = prompt(`请输入$&#123;type === &#x27;file&#x27; ? &#x27;文件&#x27; : &#x27;文件夹&#x27;&#125;名称:`)</span><br><span class="line">  if (!name) return</span><br><span class="line"></span><br><span class="line">  try &#123;</span><br><span class="line">    await window.fsAPI.createEntry(parentPath, name, type)</span><br><span class="line">    await refreshTree() // 刷新树</span><br><span class="line">  &#125; catch (e) &#123;</span><br><span class="line">    alert(&#x27;创建失败: &#x27; + e)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// === 6. 删除 ===</span><br><span class="line">const deleteItem = async () =&gt; &#123;</span><br><span class="line">  closeContextMenu()</span><br><span class="line">  const node = contextMenu.targetNode</span><br><span class="line">  if (!node || node.path === projectPath.value) return</span><br><span class="line"></span><br><span class="line">  if (!confirm(`确定要删除 $&#123;node.name&#125; 吗？`)) return</span><br><span class="line"></span><br><span class="line">  try &#123;</span><br><span class="line">    await window.fsAPI.deleteEntry(node.path)</span><br><span class="line">    await refreshTree()</span><br><span class="line">  &#125; catch (e) &#123;</span><br><span class="line">    alert(&#x27;删除失败&#x27;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">// 点击外部关闭菜单</span><br><span class="line">const onClickOutside = () =&gt; closeContextMenu()</span><br><span class="line">onMounted(() =&gt; window.addEventListener(&#x27;click&#x27;, onClickOutside))</span><br><span class="line">onUnmounted(() =&gt; window.removeEventListener(&#x27;click&#x27;, onClickOutside))</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br><span class="line">&lt;style scoped&gt;</span><br><span class="line">.explorer-container &#123;</span><br><span class="line">  height: 100%;</span><br><span class="line">  display: flex;</span><br><span class="line">  flex-direction: column;</span><br><span class="line">  background-color: #1e1e1e;</span><br><span class="line">  color: #ccc;</span><br><span class="line">  font-family: &#x27;Segoe UI&#x27;, sans-serif;</span><br><span class="line">  user-select: none;</span><br><span class="line">&#125;</span><br><span class="line">.toolbar &#123;</span><br><span class="line">  padding: 8px;</span><br><span class="line">  background-color: #252526;</span><br><span class="line">  display: flex;</span><br><span class="line">  justify-content: space-between;</span><br><span class="line">  align-items: center;</span><br><span class="line">  font-size: 12px;</span><br><span class="line">  font-weight: bold;</span><br><span class="line">&#125;</span><br><span class="line">.icon-btn &#123;</span><br><span class="line">  background: none; border: none; cursor: pointer; color: #ccc;</span><br><span class="line">&#125;</span><br><span class="line">.icon-btn:hover &#123; color: white; &#125;</span><br><span class="line">.tree-content &#123;</span><br><span class="line">  flex: 1;</span><br><span class="line">  overflow-y: auto;</span><br><span class="line">  padding-top: 4px;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/* 右键菜单样式 */</span><br><span class="line">.context-menu &#123;</span><br><span class="line">  position: fixed;</span><br><span class="line">  background: #252526;</span><br><span class="line">  border: 1px solid #454545;</span><br><span class="line">  box-shadow: 0 4px 6px rgba(0,0,0,0.3);</span><br><span class="line">  min-width: 150px;</span><br><span class="line">  z-index: 9999;</span><br><span class="line">  border-radius: 4px;</span><br><span class="line">  padding: 4px 0;</span><br><span class="line">&#125;</span><br><span class="line">.menu-item &#123;</span><br><span class="line">  padding: 6px 12px;</span><br><span class="line">  font-size: 13px;</span><br><span class="line">  cursor: pointer;</span><br><span class="line">  color: #cccccc;</span><br><span class="line">&#125;</span><br><span class="line">.menu-item:hover:not(.disabled) &#123;</span><br><span class="line">  background-color: #094771;</span><br><span class="line">  color: white;</span><br><span class="line">&#125;</span><br><span class="line">.menu-item.disabled &#123;</span><br><span class="line">  color: #666;</span><br><span class="line">  cursor: default;</span><br><span class="line">  font-size: 12px;</span><br><span class="line">  border-bottom: 1px solid #333;</span><br><span class="line">&#125;</span><br><span class="line">.menu-item.delete:hover &#123;</span><br><span class="line">  background-color: #710909;</span><br><span class="line">&#125;</span><br><span class="line">.divider &#123;</span><br><span class="line">  height: 1px;</span><br><span class="line">  background-color: #454545;</span><br><span class="line">  margin: 4px 0;</span><br><span class="line">&#125;</span><br><span class="line">&lt;/style&gt;</span><br></pre></td></tr></table></figure><hr><h3 id="如何集成到你的-Dockview"><a href="#如何集成到你的-Dockview" class="headerlink" title="如何集成到你的 Dockview"></a>如何集成到你的 Dockview</h3><p>你可以将 <code>ProjectExplorer.vue</code> 作为一个 Panel 放入 Dockview 的侧边栏（如果你的布局支持侧边栏），或者作为一个普通的 Panel。</p><p><strong>关键的联动点：</strong></p><ol><li><p><strong>文件点击</strong>：</p><ul><li>在 <code>onFileSelect</code> 中，获取到 <code>node.path</code>。</li><li>调用我们之前实现的 <code>tab-manager</code> -&gt; <code>findAndActivateTab(node.path)</code>。</li><li>如果没找到，则读取文件内容 (<code>window.fsAPI.readFile(node.path)</code>)，然后 <code>dockAPI.addPanel</code> 创建一个新的编辑器 Tab，ID 使用文件路径（确保唯一）。</li></ul></li><li><p><strong>右键创建</strong>：</p><ul><li>创建文件成功后，可以自动触发一次“打开文件”的操作，体验会更好。</li></ul></li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>这个 Demo 实现了：</p><ol><li><strong>Node.js 层</strong>：安全的 <code>fs</code> 封装，只暴露必要接口。</li><li><strong>数据层</strong>：简单的 <code>project.config.json</code> 读写。</li><li><strong>UI 层</strong>：递归文件树，支持无限层级。</li><li><strong>交互</strong>：VS Code 风格的右键菜单，支持新建和删除。</li></ol><p>这基本上就是一个轻量级 IDE 资源管理器的雏形了。你可以把 <code>ProjectExplorer.vue</code> 复制进去直接运行。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;这是一个非常硬核且实用的需求。要实现类似 VS Code &amp;#x2F; Visual Studio 的工程文件资源管理器，我们需要打通 Electron 的 &lt;strong&gt;Main Process (Node.js fs 能力)&lt;/strong&gt; 和 &lt;strong&gt;Renderer Process (Vue 组件交互)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由于代码量较大，我将分为 &lt;strong&gt;后端 (IPC&amp;#x2F;Node)&lt;/strong&gt; 和 &lt;strong&gt;前端 (Vue 组件)&lt;/strong&gt; 两部分来构建。&lt;/p&gt;</summary>
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
  <entry>
    <title>多窗口事件总线怎么处理</title>
    <link href="http://blog.ypintao.com/2025/12/16/%E5%89%8D%E7%AB%AF/electron/%E5%A4%9A%E7%AA%97%E5%8F%A3%E4%BA%8B%E4%BB%B6%E6%80%BB%E7%BA%BF%E6%80%8E%E4%B9%88%E5%A4%84%E7%90%86/"/>
    <id>http://blog.ypintao.com/2025/12/16/%E5%89%8D%E7%AB%AF/electron/%E5%A4%9A%E7%AA%97%E5%8F%A3%E4%BA%8B%E4%BB%B6%E6%80%BB%E7%BA%BF%E6%80%8E%E4%B9%88%E5%A4%84%E7%90%86/</id>
    <published>2025-12-16T13:52:57.677Z</published>
    <updated>2025-12-16T15:04:31.801Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>我现在做的是多窗口应用（基于electron-vite,vue3,和dokeview），现在有一个场景：我们原项目里面多出地方使用的事件总线模式，在组件挂载的时候注册事件，在组件销毁的时候off事件，现在改成多窗口应用后，就出现了打开了多个注册事件的窗口，事件触发的时候会触发多次，事件关闭的时候就全都关闭了。这种情况 有什么好的解决方案吗？</p><span id="more"></span><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>这是一个非常经典的架构演进问题。当你从“单页面应用 (SPA)”转变为“多窗口应用 (Multi-Window)”时，传统的事件总线（EventBus）模式如果不加改造，必然会遇到**“广播风暴”<strong>（触发多次）和</strong>“误杀”**（关一个全关了）的问题。</p><h3 id="问题根源分析"><a href="#问题根源分析" class="headerlink" title="问题根源分析"></a>问题根源分析</h3><ol><li><p><strong>触发多次 (Broadcast Storm)</strong>：</p><ul><li>如果你的事件总线是基于 IPC（<code>ipcRenderer</code>）或 <code>BroadcastChannel</code> 的，消息是广播的。</li><li>你有 3 个窗口，每个窗口里的组件都监听了 <code>refresh-data</code>。</li><li>当一个事件发出时，3 个窗口都收到了，导致代码执行了 3 次。</li><li><em>但在旧的单页应用思维里，通常预期是谁发出的，或者谁在当前视野里，谁才响应。</em></li></ul></li><li><p><strong>全都不触发了 (Premature Unsubscription)</strong>：</p><ul><li>这是最常见的问题。你的代码可能写的是 <code>bus.off(&#39;event-name&#39;)</code>。</li><li>这会移除<strong>该事件的所有监听器</strong>。</li><li>在多窗口（或同一窗口多 Tab）场景下，窗口 A 销毁组件时调用了 <code>off</code>，结果把窗口 B（甚至同窗口下的 Tab B）正在用的监听器也给删掉了。</li></ul></li></ol><hr><h3 id="解决方案-1"><a href="#解决方案-1" class="headerlink" title="解决方案"></a>解决方案</h3><p>你需要将事件总线升级为 <strong>“多级分发策略”</strong>。我们需要明确区分：<strong>这是给谁的消息？</strong></p><p>我为你提供一套 <strong>“带作用域的分布式事件总线”</strong> 解决方案。</p><h4 id="核心策略"><a href="#核心策略" class="headerlink" title="核心策略"></a>核心策略</h4><ol><li><strong>精确销毁</strong>：绝对禁止使用 <code>bus.off(&#39;name&#39;)</code>（清空所有），必须使用 <code>bus.off(&#39;name&#39;, handler)</code>（只移除当前函数的引用）。</li><li><strong>作用域区分 (Scoping)</strong>：<ul><li><strong>Local (本地)</strong>：只在当前窗口&#x2F;当前组件内生效（使用 <code>mitt</code> 或简单的回调）。</li><li><strong>Global (全局)</strong>：所有窗口都生效（例如：用户退出登录）。</li><li><strong>Targeted (定向)</strong>：只发给特定的 <code>panelId</code> 或 <code>windowId</code>（例如：刷新表格，只刷新 ID 为 <code>user-table</code> 的那个）。</li></ul></li></ol><hr><h3 id="代码实现：DistributedEventBus"><a href="#代码实现：DistributedEventBus" class="headerlink" title="代码实现：DistributedEventBus"></a>代码实现：<code>DistributedEventBus</code></h3><p>我们将封装一个智能的 EventBus，它底层结合了 <code>mitt</code> (本地) 和 <code>BroadcastChannel</code> (跨窗口)。</p><h4 id="1-工具类封装-src-utils-event-bus-ts"><a href="#1-工具类封装-src-utils-event-bus-ts" class="headerlink" title="1. 工具类封装 (src/utils/event-bus.ts)"></a>1. 工具类封装 (<code>src/utils/event-bus.ts</code>)</h4><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> mitt, &#123; <span class="title class_">Handler</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;mitt&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义消息结构</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">BusPayload</span> = &#123;</span><br><span class="line">  <span class="attr">event</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="attr">data</span>: <span class="built_in">any</span></span><br><span class="line">  <span class="attr">targetId</span>?: <span class="built_in">string</span> <span class="comment">// 指定接收目标的 ID (PanelId 或 WindowId)</span></span><br><span class="line">  <span class="attr">sourceId</span>?: <span class="built_in">string</span> <span class="comment">// 发送者的 ID</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">DistributedEventBus</span> &#123;</span><br><span class="line">  <span class="keyword">private</span> localBus = <span class="title function_">mitt</span>() <span class="comment">// 本地事件中心</span></span><br><span class="line">  <span class="keyword">private</span> <span class="attr">channel</span>: <span class="title class_">BroadcastChannel</span></span><br><span class="line">  <span class="keyword">private</span> <span class="attr">myId</span>: <span class="built_in">string</span> <span class="comment">// 当前实例/窗口的 ID</span></span><br><span class="line"></span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"><span class="attr">channelName</span>: <span class="built_in">string</span> = <span class="string">&#x27;app-global-bus&#x27;</span></span>) &#123;</span><br><span class="line">    <span class="comment">// 生成或获取当前环境 ID (可以使用之前 Pinia 插件里的 generateId 逻辑)</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">myId</span> = <span class="title class_">Math</span>.<span class="title function_">random</span>().<span class="title function_">toString</span>(<span class="number">36</span>).<span class="title function_">substr</span>(<span class="number">2</span>, <span class="number">9</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 初始化跨窗口通道</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">channel</span> = <span class="keyword">new</span> <span class="title class_">BroadcastChannel</span>(channelName)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 监听来自其他窗口的消息</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">channel</span>.<span class="property">onmessage</span> = <span class="function">(<span class="params"><span class="attr">msg</span>: <span class="title class_">MessageEvent</span>&lt;<span class="title class_">BusPayload</span>&gt;</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> &#123; event, data, targetId, sourceId &#125; = msg.<span class="property">data</span></span><br><span class="line"></span><br><span class="line">      <span class="comment">// A. 如果指定了 targetId，且不是发给我的 -&gt; 忽略</span></span><br><span class="line">      <span class="comment">// (这里假设 myId 是窗口ID，或者是你传入的 panelId)</span></span><br><span class="line">      <span class="keyword">if</span> (targetId &amp;&amp; targetId !== <span class="variable language_">this</span>.<span class="property">myId</span>) <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">      <span class="comment">// B. 触发本地监听器</span></span><br><span class="line">      <span class="comment">// 注意：我们在本地触发时，标记它来自&quot;外部&quot;，避免死循环</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">localBus</span>.<span class="title function_">emit</span>(event, &#123; ...data, <span class="attr">_fromRemote</span>: <span class="literal">true</span> &#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></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">on</span>(<span class="params"><span class="attr">event</span>: <span class="built_in">string</span>, <span class="attr">handler</span>: <span class="title class_">Handler</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">localBus</span>.<span class="title function_">on</span>(event, handler)</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">   * 移除监听 (必须传入 handler 引用！)</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">off</span>(<span class="params"><span class="attr">event</span>: <span class="built_in">string</span>, <span class="attr">handler</span>: <span class="title class_">Handler</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">localBus</span>.<span class="title function_">off</span>(event, handler)</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 class="doctag">@param</span> event 事件名</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> data 数据</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> scope 发送范围</span></span><br><span class="line"><span class="comment">   *    - &#x27;local&#x27;: 只在当前窗口触发 (默认)</span></span><br><span class="line"><span class="comment">   *    - &#x27;global&#x27;: 所有窗口都触发</span></span><br><span class="line"><span class="comment">   *    - string: 指定 ID (只发给特定 ID 的实例)</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">emit</span>(<span class="params"><span class="attr">event</span>: <span class="built_in">string</span>, <span class="attr">data</span>: <span class="built_in">any</span> = &#123;&#125;, <span class="attr">scope</span>: <span class="string">&#x27;local&#x27;</span> | <span class="string">&#x27;global&#x27;</span> | <span class="built_in">string</span> = <span class="string">&#x27;local&#x27;</span></span>) &#123;</span><br><span class="line">    <span class="comment">// 1. 总是触发本地 (除非有特殊需求)</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">localBus</span>.<span class="title function_">emit</span>(event, data)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 根据范围决定是否广播</span></span><br><span class="line">    <span class="keyword">if</span> (scope === <span class="string">&#x27;local&#x27;</span>) <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 构造广播包</span></span><br><span class="line">    <span class="keyword">const</span> <span class="attr">payload</span>: <span class="title class_">BusPayload</span> = &#123;</span><br><span class="line">      event,</span><br><span class="line">      data,</span><br><span class="line">      <span class="attr">sourceId</span>: <span class="variable language_">this</span>.<span class="property">myId</span>,</span><br><span class="line">      <span class="attr">targetId</span>: scope === <span class="string">&#x27;global&#x27;</span> ? <span class="literal">undefined</span> : scope <span class="comment">// 如果是 global 则 targetId 为空</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">channel</span>.<span class="title function_">postMessage</span>(payload)</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 设置当前实例的 ID (用于 Dockview 的 Panel ID 绑定)</span></span><br><span class="line">  <span class="title function_">setId</span>(<span class="params"><span class="attr">id</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">myId</span> = id</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">destroy</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">localBus</span>.<span class="property">all</span>.<span class="title function_">clear</span>()</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">channel</span>.<span class="title function_">close</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="comment">// 导出单例 (或者在组件里 new)</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> bus = <span class="keyword">new</span> <span class="title class_">DistributedEventBus</span>()</span><br></pre></td></tr></table></figure><hr><h4 id="2-在-Vue-组件中如何正确使用-解决“全关闭”问题"><a href="#2-在-Vue-组件中如何正确使用-解决“全关闭”问题" class="headerlink" title="2. 在 Vue 组件中如何正确使用 (解决“全关闭”问题)"></a>2. 在 Vue 组件中如何正确使用 (解决“全关闭”问题)</h4><p>关键在于：<strong>在 <code>onUnmounted</code> 时，只移除自己的那个回调函数</strong>。</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><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></pre></td><td class="code"><pre><span class="line">&lt;script setup lang=&quot;ts&quot;&gt;</span><br><span class="line">import &#123; onMounted, onUnmounted &#125; from &#x27;vue&#x27;</span><br><span class="line">import &#123; bus &#125; from &#x27;./utils/event-bus&#x27;</span><br><span class="line"></span><br><span class="line">// 假设这是 Dockview 里的一个 Panel</span><br><span class="line">const props = defineProps&lt;&#123; params: any &#125;&gt;()</span><br><span class="line">const myPanelId = props.params.api.id</span><br><span class="line"></span><br><span class="line">// 1. 设置 ID，以便能接收定向消息</span><br><span class="line">bus.setId(myPanelId)</span><br><span class="line"></span><br><span class="line">// 2. 定义处理函数 (必须提取出来，不能写匿名函数！)</span><br><span class="line">// ❌ 错误：bus.on(&#x27;refresh&#x27;, () =&gt; &#123; ... &#125;)  &lt;- 匿名函数没法精确移除</span><br><span class="line">// ✅ 正确：</span><br><span class="line">const handleRefresh = (data: any) =&gt; &#123;</span><br><span class="line">  console.log(`[$&#123;myPanelId&#125;] 收到刷新请求:`, data)</span><br><span class="line">  // 业务逻辑...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">onMounted(() =&gt; &#123;</span><br><span class="line">  // 注册</span><br><span class="line">  bus.on(&#x27;refresh-grid&#x27;, handleRefresh)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">onUnmounted(() =&gt; &#123;</span><br><span class="line">  // 3. 销毁时，只移除当前的 handleRefresh</span><br><span class="line">  // 这样绝对不会影响其他窗口或 Tab 的 &#x27;refresh-grid&#x27; 监听</span><br><span class="line">  bus.off(&#x27;refresh-grid&#x27;, handleRefresh)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">// --- 触发示例 ---</span><br><span class="line"></span><br><span class="line">const triggerLocal = () =&gt; &#123;</span><br><span class="line">  // 只刷新我自己这个窗口/Tab</span><br><span class="line">  bus.emit(&#x27;refresh-grid&#x27;, &#123; page: 1 &#125;, &#x27;local&#x27;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const triggerGlobal = () =&gt; &#123;</span><br><span class="line">  // 刷新所有打开这个页面的窗口 (比如用户修改了全局配置)</span><br><span class="line">  bus.emit(&#x27;refresh-grid&#x27;, &#123; theme: &#x27;dark&#x27; &#125;, &#x27;global&#x27;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const triggerTarget = () =&gt; &#123;</span><br><span class="line">  // 假设你想刷新 ID 为 &#x27;user-list-panel&#x27; 的那个 Tab (无论它在哪个窗口)</span><br><span class="line">  bus.emit(&#x27;refresh-grid&#x27;, &#123; reload: true &#125;, &#x27;user-list-panel&#x27;)</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><hr><h3 id="方案优缺点总结"><a href="#方案优缺点总结" class="headerlink" title="方案优缺点总结"></a>方案优缺点总结</h3><ol><li><p><strong>解决“触发多次”</strong>：</p><ul><li>默认 <code>scope: &#39;local&#39;</code>：大多数业务场景（比如点击按钮刷新当前表格）只需要本地触发。旧代码如果不改，默认就是 Local，行为和单页应用一致，不会广播风暴。</li><li>需要同步时显式用 <code>&#39;global&#39;</code>。</li></ul></li><li><p><strong>解决“全部关闭”</strong>：</p><ul><li>强制要求传入 <code>handler</code> 进行 <code>off</code>。这是编程习惯的矫正。</li><li><code>mitt</code> 库本身就支持精确移除。</li></ul></li><li><p><strong>适配 Dockview</strong>：</p><ul><li>通过 <code>setId(panelId)</code>，实现了基于 <strong>Panel ID</strong> 的定向通信。</li><li>你可以实现：“在窗口 A 点击保存，自动刷新窗口 B 里的列表”这种高级功能。</li></ul></li></ol><h3 id="迁移建议"><a href="#迁移建议" class="headerlink" title="迁移建议"></a>迁移建议</h3><p>你现在的项目里可能充斥着 <code>bus.emit</code> 和 <code>bus.off</code>。</p><ol><li><strong>搜索替换</strong>：把引入的老 EventBus 换成这个新的 <code>DistributedEventBus</code>。</li><li><strong>默认行为</strong>：由于默认是 <code>&#39;local&#39;</code>，你直接替换后，大部分功能会恢复正常（不再乱广播）。</li><li><strong>修复销毁逻辑</strong>：你需要人工检查所有 <code>onUnmounted</code> 或 <code>beforeDestroy</code>，把 <code>bus.off(&#39;event&#39;)</code> 改成 <code>bus.off(&#39;event&#39;, myHandler)</code>。<ul><li><em>如果老代码全是匿名函数怎么办？</em></li><li><strong>临时补救</strong>：在 <code>DistributedEventBus</code> 里维护一个 <code>Map&lt;PanelId, List&lt;Handler&gt;&gt;</code>，在组件销毁时调用 <code>bus.clearCurrentScope()</code>。但这比较复杂，推荐还是重构为命名函数。</li></ul></li></ol>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;问题&quot;&gt;&lt;a href=&quot;#问题&quot; class=&quot;headerlink&quot; title=&quot;问题&quot;&gt;&lt;/a&gt;问题&lt;/h2&gt;&lt;p&gt;我现在做的是多窗口应用（基于electron-vite,vue3,和dokeview），现在有一个场景：我们原项目里面多出地方使用的事件总线模式，在组件挂载的时候注册事件，在组件销毁的时候off事件，现在改成多窗口应用后，就出现了打开了多个注册事件的窗口，事件触发的时候会触发多次，事件关闭的时候就全都关闭了。这种情况 有什么好的解决方案吗？&lt;/p&gt;</summary>
    
    
    
    <category term="electron" scheme="http://blog.ypintao.com/categories/electron/"/>
    
    
    <category term="electron" scheme="http://blog.ypintao.com/tags/electron/"/>
    
  </entry>
  
</feed>
