<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>刘立陈</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://blogs.microcyan.com/</id>
  <link href="https://blogs.microcyan.com/" rel="alternate"/>
  <link href="https://blogs.microcyan.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 刘立陈</rights>
  <subtitle>记录实践、排查与工程思考</subtitle>
  <title>Leroi 的技术博客</title>
  <updated>2026-09-08T10:34:43.299Z</updated>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="安全与逆向工程" scheme="https://blogs.microcyan.com/categories/security/"/>
    <category term="微信" scheme="https://blogs.microcyan.com/tags/%E5%BE%AE%E4%BF%A1/"/>
    <category term="抖音" scheme="https://blogs.microcyan.com/tags/%E6%8A%96%E9%9F%B3/"/>
    <category term="Android" scheme="https://blogs.microcyan.com/tags/Android/"/>
    <category term="iOS" scheme="https://blogs.microcyan.com/tags/iOS/"/>
    <category term="小红书" scheme="https://blogs.microcyan.com/tags/%E5%B0%8F%E7%BA%A2%E4%B9%A6/"/>
    <content>
      <![CDATA[<!-- more --><p>URL Scheme 常用于从浏览器、短信、H5、App 内 WebView 或另一个 App 拉起目标 App。不同版本、不同平台、不同地区包名可能会变，这份清单适合作为调试入口，最终以你本机安装版本验证为准。</p><h2 id="验证方法"><a href="#验证方法" class="headerlink" title="验证方法"></a>验证方法</h2><h3 id="Android-使用-adb-打开"><a href="#Android-使用-adb-打开" class="headerlink" title="Android 使用 adb 打开"></a>Android 使用 adb 打开</h3><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -a android.intent.action.VIEW -d <span class="string">'snssdk1128://'</span></span><br></pre></td></tr></tbody></table></figure><p>打开指定路径：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -a android.intent.action.VIEW -d <span class="string">'weixin://'</span></span><br></pre></td></tr></tbody></table></figure><p>如果要看失败原因：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb logcat | grep -i <span class="string">'ActivityTaskManager'</span></span><br></pre></td></tr></tbody></table></figure><p>常见失败：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Error: Activity not started, unable to resolve Intent</span><br></pre></td></tr></tbody></table></figure><p>意思是当前设备没有 App 声明能处理这个 scheme，或者 scheme 已经变化。</p><h3 id="iOS-模拟器打开"><a href="#iOS-模拟器打开" class="headerlink" title="iOS 模拟器打开"></a>iOS 模拟器打开</h3><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">xcrun simctl openurl booted <span class="string">'weixin://'</span></span><br></pre></td></tr></tbody></table></figure><p>真机可以在 Safari 地址栏里输入 scheme，也可以用自己写的测试 App 调 <code>UIApplication.open</code>。</p><h3 id="从-APK-里查-scheme"><a href="#从-APK-里查-scheme" class="headerlink" title="从 APK 里查 scheme"></a>从 APK 里查 scheme</h3><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">apktool d app.apk -o app_dec</span><br><span class="line">rg -n <span class="string">'scheme|host|intent-filter'</span> app_dec/AndroidManifest.xml</span><br></pre></td></tr></tbody></table></figure><p>典型结构：</p><figure class="highlight xml"><table><tbody><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="tag">&lt;<span class="name">intent-filter</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.VIEW"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.BROWSABLE"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">data</span> <span class="attr">android:scheme</span>=<span class="string">"snssdk1128"</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">intent-filter</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><h3 id="从-iOS-Info-plist-查-scheme"><a href="#从-iOS-Info-plist-查-scheme" class="headerlink" title="从 iOS Info.plist 查 scheme"></a>从 iOS Info.plist 查 scheme</h3><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">plutil -p Payload/App.app/Info.plist | grep -A 20 CFBundleURLTypes</span><br></pre></td></tr></tbody></table></figure><p>也可以把 plist 转成 XML：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">plutil -convert xml1 Payload/App.app/Info.plist -o -</span><br></pre></td></tr></tbody></table></figure><h2 id="常见-App-Scheme-清单"><a href="#常见-App-Scheme-清单" class="headerlink" title="常见 App Scheme 清单"></a>常见 App Scheme 清单</h2><div class="md-table-scroll"><table><thead><tr><th>App</th><th>常见 Scheme</th><th>备注</th></tr></thead><tbody><tr><td>抖音</td><td><code>snssdk1128://</code></td><td>国内抖音常见主 scheme</td></tr><tr><td>抖音极速版</td><td><code>snssdk2329://</code></td><td>具体以安装包为准</td></tr><tr><td>TikTok</td><td><code>snssdk1233://</code></td><td>海外 TikTok 常见</td></tr><tr><td>快手</td><td><code>kwai://</code></td><td>快手常见</td></tr><tr><td>快手极速版</td><td><code>ksnebula://</code></td><td>具体以版本为准</td></tr><tr><td>小红书</td><td><code>xhsdiscover://</code></td><td>小红书常见</td></tr><tr><td>微信</td><td><code>weixin://</code></td><td>微信主 scheme</td></tr><tr><td>企业微信</td><td><code>wxwork://</code></td><td>企业微信</td></tr><tr><td>微博</td><td><code>sinaweibo://</code></td><td>新浪微博</td></tr><tr><td>B站</td><td><code>bilibili://</code></td><td>哔哩哔哩</td></tr><tr><td>QQ</td><td><code>mqq://</code></td><td>手机 QQ</td></tr><tr><td>QQ 浏览器</td><td><code>mttbrowser://</code></td><td>QQ 浏览器</td></tr><tr><td>支付宝</td><td><code>alipays://</code></td><td>支付宝</td></tr><tr><td>淘宝</td><td><code>taobao://</code>、<code>tbopen://</code></td><td>淘宝和手淘跳转</td></tr><tr><td>京东</td><td><code>openapp.jdmobile://</code></td><td>京东</td></tr><tr><td>拼多多</td><td><code>pinduoduo://</code></td><td>拼多多</td></tr><tr><td>美团</td><td><code>imeituan://</code></td><td>美团</td></tr><tr><td>大众点评</td><td><code>dianping://</code></td><td>大众点评</td></tr><tr><td>高德地图</td><td><code>iosamap://</code>、<code>androidamap://</code></td><td>iOS/Android 不同</td></tr><tr><td>百度地图</td><td><code>baidumap://</code></td><td>百度地图</td></tr><tr><td>知乎</td><td><code>zhihu://</code></td><td>知乎</td></tr><tr><td>豆瓣</td><td><code>douban://</code></td><td>豆瓣</td></tr><tr><td>网易云音乐</td><td><code>orpheus://</code></td><td>网易云音乐</td></tr><tr><td>喜马拉雅</td><td><code>iting://</code></td><td>喜马拉雅</td></tr></tbody></table></div><h2 id="抖音"><a href="#抖音" class="headerlink" title="抖音"></a>抖音</h2><p>常见入口：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">snssdk1128://</span><br></pre></td></tr></tbody></table></figure><p>Android 测试：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -a android.intent.action.VIEW -d <span class="string">'snssdk1128://'</span></span><br></pre></td></tr></tbody></table></figure><p>常见包名：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">com.ss.android.ugc.aweme</span><br></pre></td></tr></tbody></table></figure><p>查看抖音声明了哪些 intent：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell dumpsys package com.ss.android.ugc.aweme | grep -i -A 8 <span class="string">'scheme'</span></span><br></pre></td></tr></tbody></table></figure><p>如果想从网页里兜底打开：</p><figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"snssdk1128://"</span>&gt;</span>打开抖音<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>实际业务里通常还会配合 Universal Link、App Link 或下载页兜底。</p><h2 id="快手"><a href="#快手" class="headerlink" title="快手"></a>快手</h2><p>常见入口：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kwai://</span><br></pre></td></tr></tbody></table></figure><p>Android 测试：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -a android.intent.action.VIEW -d <span class="string">'kwai://'</span></span><br></pre></td></tr></tbody></table></figure><p>常见包名：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">com.smile.gifmaker</span><br></pre></td></tr></tbody></table></figure><p>如果 <code>kwai://</code> 无法打开，先看安装包 manifest：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell pm path com.smile.gifmaker</span><br></pre></td></tr></tbody></table></figure><p>导出 APK 后再用 <code>apktool</code> 查 <code>intent-filter</code>。</p><h2 id="小红书"><a href="#小红书" class="headerlink" title="小红书"></a>小红书</h2><p>常见入口：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">xhsdiscover://</span><br></pre></td></tr></tbody></table></figure><p>Android 测试：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -a android.intent.action.VIEW -d <span class="string">'xhsdiscover://'</span></span><br></pre></td></tr></tbody></table></figure><p>常见包名：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">com.xingin.xhs</span><br></pre></td></tr></tbody></table></figure><p>调试时可以先只打开主 scheme，不要一开始就拼复杂路径。主 scheme 能打开，再逐步加 host 和 query。</p><h2 id="微信"><a href="#微信" class="headerlink" title="微信"></a>微信</h2><p>常见入口：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">weixin://</span><br></pre></td></tr></tbody></table></figure><p>Android 测试：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -a android.intent.action.VIEW -d <span class="string">'weixin://'</span></span><br></pre></td></tr></tbody></table></figure><p>常见包名：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">com.tencent.mm</span><br></pre></td></tr></tbody></table></figure><p>微信对很多内部路径有限制，能否跳到具体页面通常受版本、来源、白名单、Universal Link 配置影响。调试开放能力时优先看微信开放平台文档和你自己的 App 配置。</p><h2 id="微博"><a href="#微博" class="headerlink" title="微博"></a>微博</h2><p>常见入口：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sinaweibo://</span><br></pre></td></tr></tbody></table></figure><p>Android 测试：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -a android.intent.action.VIEW -d <span class="string">'sinaweibo://'</span></span><br></pre></td></tr></tbody></table></figure><p>常见包名：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">com.sina.weibo</span><br></pre></td></tr></tbody></table></figure><p>网页按钮：</p><figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"sinaweibo://"</span>&gt;</span>打开微博<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><h2 id="B站"><a href="#B站" class="headerlink" title="B站"></a>B站</h2><p>常见入口：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bilibili://</span><br></pre></td></tr></tbody></table></figure><p>Android 测试：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -a android.intent.action.VIEW -d <span class="string">'bilibili://'</span></span><br></pre></td></tr></tbody></table></figure><p>常见包名：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tv.danmaku.bili</span><br></pre></td></tr></tbody></table></figure><p>如果主 scheme 成功但具体视频路径失败，说明路径规则不匹配或被版本限制。先用 <code>dumpsys package</code> 查声明，再抓取 App 内分享链接做对照。</p><h2 id="H5-拉起-App-的兜底写法"><a href="#H5-拉起-App-的兜底写法" class="headerlink" title="H5 拉起 App 的兜底写法"></a>H5 拉起 App 的兜底写法</h2><p>简单版：</p><figure class="highlight html"><table><tbody><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="tag">&lt;<span class="name">button</span> <span class="attr">id</span>=<span class="string">"openApp"</span>&gt;</span>打开 App<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> scheme = <span class="string">'bilibili://'</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> fallback = <span class="string">'https://www.bilibili.com/'</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">'openApp'</span>).<span class="title function_">addEventListener</span>(<span class="string">'click'</span>, <span class="function">() =&gt;</span> {</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">const</span> started = <span class="title class_">Date</span>.<span class="title function_">now</span>()</span></span><br><span class="line"><span class="language-javascript">  <span class="variable language_">window</span>.<span class="property">location</span>.<span class="property">href</span> = scheme</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> {</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">if</span> (<span class="title class_">Date</span>.<span class="title function_">now</span>() - started &lt; <span class="number">1800</span>) {</span></span><br><span class="line"><span class="language-javascript">      <span class="variable language_">window</span>.<span class="property">location</span>.<span class="property">href</span> = fallback</span></span><br><span class="line"><span class="language-javascript">    }</span></span><br><span class="line"><span class="language-javascript">  }, <span class="number">1200</span>)</span></span><br><span class="line"><span class="language-javascript">})</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>更稳一点的版本要监听页面可见性：</p><figure class="highlight html"><table><tbody><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="tag">&lt;<span class="name">button</span> <span class="attr">id</span>=<span class="string">"openApp"</span>&gt;</span>打开 App<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">function</span> <span class="title function_">openWithFallback</span>(<span class="params">scheme, fallback</span>) {</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">let</span> hidden = <span class="literal">false</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">const</span> <span class="title function_">onVisibility</span> = (<span class="params"></span>) =&gt; {</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">if</span> (<span class="variable language_">document</span>.<span class="property">hidden</span>) {</span></span><br><span class="line"><span class="language-javascript">      hidden = <span class="literal">true</span></span></span><br><span class="line"><span class="language-javascript">    }</span></span><br><span class="line"><span class="language-javascript">  }</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="variable language_">document</span>.<span class="title function_">addEventListener</span>(<span class="string">'visibilitychange'</span>, onVisibility)</span></span><br><span class="line"><span class="language-javascript">  <span class="variable language_">window</span>.<span class="property">location</span>.<span class="property">href</span> = scheme</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">  <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> {</span></span><br><span class="line"><span class="language-javascript">    <span class="variable language_">document</span>.<span class="title function_">removeEventListener</span>(<span class="string">'visibilitychange'</span>, onVisibility)</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">if</span> (!hidden) {</span></span><br><span class="line"><span class="language-javascript">      <span class="variable language_">window</span>.<span class="property">location</span>.<span class="property">href</span> = fallback</span></span><br><span class="line"><span class="language-javascript">    }</span></span><br><span class="line"><span class="language-javascript">  }, <span class="number">1500</span>)</span></span><br><span class="line"><span class="language-javascript">}</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">'openApp'</span>).<span class="title function_">addEventListener</span>(<span class="string">'click'</span>, <span class="function">() =&gt;</span> {</span></span><br><span class="line"><span class="language-javascript">  <span class="title function_">openWithFallback</span>(<span class="string">'xhsdiscover://'</span>, <span class="string">'https://www.xiaohongshu.com/'</span>)</span></span><br><span class="line"><span class="language-javascript">})</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><h2 id="Android-侧判断能否打开"><a href="#Android-侧判断能否打开" class="headerlink" title="Android 侧判断能否打开"></a>Android 侧判断能否打开</h2><p>Kotlin：</p><figure class="highlight kotlin"><table><tbody><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="function"><span class="keyword">fun</span> <span class="title">canOpen</span><span class="params">(context: <span class="type">Context</span>, uri: <span class="type">String</span>)</span></span>: <span class="built_in">Boolean</span> {</span><br><span class="line">    <span class="keyword">val</span> intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))</span><br><span class="line">    intent.addCategory(Intent.CATEGORY_BROWSABLE)</span><br><span class="line">    <span class="keyword">return</span> intent.resolveActivity(context.packageManager) != <span class="literal">null</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">openScheme</span><span class="params">(context: <span class="type">Context</span>, uri: <span class="type">String</span>)</span></span> {</span><br><span class="line">    <span class="keyword">val</span> intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))</span><br><span class="line">    intent.addCategory(Intent.CATEGORY_BROWSABLE)</span><br><span class="line">    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)</span><br><span class="line">    context.startActivity(intent)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>Android 11 以后如果要查询其他 App，需要在 <code>AndroidManifest.xml</code> 里声明 <code>queries</code>：</p><figure class="highlight xml"><table><tbody><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="tag">&lt;<span class="name">queries</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">package</span> <span class="attr">android:name</span>=<span class="string">"com.tencent.mm"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">package</span> <span class="attr">android:name</span>=<span class="string">"com.ss.android.ugc.aweme"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">intent</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.VIEW"</span> /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">data</span> <span class="attr">android:scheme</span>=<span class="string">"weixin"</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">intent</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">queries</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><h2 id="iOS-侧判断能否打开"><a href="#iOS-侧判断能否打开" class="headerlink" title="iOS 侧判断能否打开"></a>iOS 侧判断能否打开</h2><p>Swift：</p><figure class="highlight swift"><table><tbody><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">func</span> <span class="title function_">openScheme</span>(<span class="keyword">_</span> <span class="params">value</span>: <span class="type">String</span>) {</span><br><span class="line">    <span class="keyword">guard</span> <span class="keyword">let</span> url <span class="operator">=</span> <span class="type">URL</span>(string: value) <span class="keyword">else</span> {</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="type">UIApplication</span>.shared.open(url)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>如果要用 <code>canOpenURL</code>，需要在 <code>Info.plist</code> 配置白名单：</p><figure class="highlight xml"><table><tbody><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="tag">&lt;<span class="name">key</span>&gt;</span>LSApplicationQueriesSchemes<span class="tag">&lt;/<span class="name">key</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">array</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span>&gt;</span>weixin<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span>&gt;</span>sinaweibo<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span>&gt;</span>bilibili<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span>&gt;</span>xhsdiscover<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span>&gt;</span>snssdk1128<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span>&gt;</span>kwai<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">array</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>Swift：</p><figure class="highlight swift"><table><tbody><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">func</span> <span class="title function_">canOpen</span>(<span class="keyword">_</span> <span class="params">value</span>: <span class="type">String</span>) -&gt; <span class="type">Bool</span> {</span><br><span class="line">    <span class="keyword">guard</span> <span class="keyword">let</span> url <span class="operator">=</span> <span class="type">URL</span>(string: value) <span class="keyword">else</span> {</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    }</span><br><span class="line">    <span class="keyword">return</span> <span class="type">UIApplication</span>.shared.canOpenURL(url)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="排查清单"><a href="#排查清单" class="headerlink" title="排查清单"></a>排查清单</h2><div class="md-table-scroll"><table><thead><tr><th>现象</th><th>排查方向</th></tr></thead><tbody><tr><td>Android 报 <code>unable to resolve Intent</code></td><td>App 未安装、scheme 错、manifest 未声明 browsable</td></tr><tr><td>iOS <code>canOpenURL</code> 返回 false</td><td>没配 <code>LSApplicationQueriesSchemes</code>、App 未安装、scheme 错</td></tr><tr><td>H5 点击无反应</td><td>浏览器限制非用户手势拉起、scheme 被拦截</td></tr><tr><td>能打开 App 但不到具体页面</td><td>host/path/query 规则变了，或目标页需要登录态</td></tr><tr><td>某些浏览器可以，某些不行</td><td>浏览器对外部协议策略不同</td></tr><tr><td>Android 11 查询不到</td><td>缺少 <code>queries</code> 声明</td></tr></tbody></table></div><h2 id="建议保存自己的验证表"><a href="#建议保存自己的验证表" class="headerlink" title="建议保存自己的验证表"></a>建议保存自己的验证表</h2><p>scheme 资料很容易过期。每次用于项目时，建议记录：</p><div class="md-table-scroll"><table><thead><tr><th>字段</th><th>示例</th></tr></thead><tbody><tr><td>App 名称</td><td>抖音</td></tr><tr><td>App 版本</td><td>32.x</td></tr><tr><td>平台</td><td>Android</td></tr><tr><td>包名</td><td><code>com.ss.android.ugc.aweme</code></td></tr><tr><td>Scheme</td><td><code>snssdk1128://</code></td></tr><tr><td>是否可打开</td><td>是</td></tr><tr><td>验证命令</td><td><code>adb shell am start -a android.intent.action.VIEW -d 'snssdk1128://'</code></td></tr><tr><td>验证日期</td><td>2026-05-31</td></tr></tbody></table></div><p>这样以后出问题时，能快速判断是代码变了、App 版本变了，还是设备环境变了。</p>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/security-mobile-app-url-schemes/</id>
    <link href="https://blogs.microcyan.com/posts/security-mobile-app-url-schemes/"/>
    <published>2026-06-22T13:09:00.000Z</published>
    <summary>抖音、快手、小红书、微信、微博、B站等常见 App URL Scheme、Android intent、iOS 打开方式、验证命令和排查方法整理。</summary>
    <title>常见移动 App URL Scheme 整理</title>
    <updated>2026-09-08T10:34:43.299Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="Grafana" scheme="https://blogs.microcyan.com/tags/Grafana/"/>
    <category term="Docker" scheme="https://blogs.microcyan.com/tags/Docker/"/>
    <category term="Loki" scheme="https://blogs.microcyan.com/tags/Loki/"/>
    <category term="可观测性" scheme="https://blogs.microcyan.com/tags/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/"/>
    <category term="系统监控" scheme="https://blogs.microcyan.com/tags/%E7%B3%BB%E7%BB%9F%E7%9B%91%E6%8E%A7/"/>
    <content>
      <![CDATA[<!-- more --><p>Loki 是 Grafana 生态里的日志系统。它和 Elasticsearch 最大的区别是：Loki 不默认给日志全文建立复杂索引，而是主要索引标签，因此成本更低，但查询方式也更依赖标签设计。</p><blockquote><p><strong>官方入口</strong></p><ul><li><a target="_blank" rel="noopener" href="https://grafana.com/docs/loki/latest/">Loki 文档</a></li><li><a target="_blank" rel="noopener" href="https://grafana.com/docs/loki/latest/get-started/">Loki 入门</a></li><li><a target="_blank" rel="noopener" href="https://grafana.com/docs/loki/latest/setup/install/docker/">Loki Docker 安装</a></li></ul></blockquote><h2 id="Loki-适合什么"><a href="#Loki-适合什么" class="headerlink" title="Loki 适合什么"></a>Loki 适合什么</h2><p>适合：</p><ul><li>容器日志。</li><li>Nginx、应用、任务日志。</li><li>和 Grafana 一起查询日志。</li><li>中小规模日志平台。</li><li>成本敏感的日志存储。</li></ul><p>不适合：</p><ul><li>需要大量任意字段全文索引。</li><li>类似搜索引擎一样对所有字段做复杂检索。</li><li>标签设计混乱、日志没有结构。</li></ul><h2 id="核心概念"><a href="#核心概念" class="headerlink" title="核心概念"></a>核心概念</h2><div class="md-table-scroll"><table><thead><tr><th>概念</th><th>说明</th></tr></thead><tbody><tr><td>Loki</td><td>日志存储与查询服务</td></tr><tr><td>Grafana</td><td>查询和展示 Loki 日志</td></tr><tr><td>Promtail / Alloy</td><td>日志采集客户端</td></tr><tr><td>LogQL</td><td>Loki 查询语言</td></tr><tr><td>Label</td><td>标签，用于定位日志流</td></tr><tr><td>Stream</td><td>一组相同标签的日志流</td></tr></tbody></table></div><h2 id="Docker-快速启动"><a href="#Docker-快速启动" class="headerlink" title="Docker 快速启动"></a>Docker 快速启动</h2><p>开发测试可以用单容器体验：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">  --name loki \</span><br><span class="line">  -p 3100:3100 \</span><br><span class="line">  grafana/loki:3.6.0</span><br></pre></td></tr></tbody></table></figure><p>验证：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl http://localhost:3100/ready</span><br></pre></td></tr></tbody></table></figure><p>Grafana 添加 Loki 数据源：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://loki:3100</span><br></pre></td></tr></tbody></table></figure><p>如果 Grafana 不在同一个 Docker 网络里，则使用宿主机 IP 或域名。</p><h2 id="LogQL-入门"><a href="#LogQL-入门" class="headerlink" title="LogQL 入门"></a>LogQL 入门</h2><p>按标签查询：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{job="nginx"}</span><br></pre></td></tr></tbody></table></figure><p>包含关键词：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{job="nginx"} |= "error"</span><br></pre></td></tr></tbody></table></figure><p>排除关键词：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{job="nginx"} != "health"</span><br></pre></td></tr></tbody></table></figure><p>正则匹配：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{job="nginx"} |~ "5.."</span><br></pre></td></tr></tbody></table></figure><p>统计日志行数：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sum(count_over_time({job="nginx"}[5m]))</span><br></pre></td></tr></tbody></table></figure><p>按标签分组统计：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sum by (level) (count_over_time({app="api"}[5m]))</span><br></pre></td></tr></tbody></table></figure><h2 id="标签设计"><a href="#标签设计" class="headerlink" title="标签设计"></a>标签设计</h2><p>适合作为标签：</p><ul><li><code>app</code></li><li><code>env</code></li><li><code>job</code></li><li><code>host</code></li><li><code>namespace</code></li><li><code>container</code></li><li><code>level</code></li></ul><p>不适合作为标签：</p><ul><li>请求 ID。</li><li>用户 ID。</li><li>订单号。</li><li>完整 URL。</li><li>随机字符串。</li><li>IP 明细。</li></ul><p>标签过多或高基数会让 Loki 压力变大。日志正文里可以有很多字段，但标签要克制。</p><h2 id="应用日志建议"><a href="#应用日志建议" class="headerlink" title="应用日志建议"></a>应用日志建议</h2><p>推荐结构化日志：</p><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span><span class="attr">"time"</span><span class="punctuation">:</span><span class="string">"2026-05-28T12:00:00Z"</span><span class="punctuation">,</span><span class="attr">"level"</span><span class="punctuation">:</span><span class="string">"error"</span><span class="punctuation">,</span><span class="attr">"service"</span><span class="punctuation">:</span><span class="string">"api"</span><span class="punctuation">,</span><span class="attr">"message"</span><span class="punctuation">:</span><span class="string">"database timeout"</span><span class="punctuation">,</span><span class="attr">"trace_id"</span><span class="punctuation">:</span><span class="string">"abc123"</span><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><p>应用侧至少要包含：</p><ul><li>时间。</li><li>等级。</li><li>服务名。</li><li>错误信息。</li><li>请求 ID 或 trace ID。</li><li>关键业务上下文。</li></ul><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Grafana-查不到日志"><a href="#Grafana-查不到日志" class="headerlink" title="Grafana 查不到日志"></a>Grafana 查不到日志</h3><p>排查：</p><ul><li>Loki 是否启动。</li><li>Grafana 数据源 URL 是否正确。</li><li>采集器是否把日志推到了 Loki。</li><li>查询时间范围是否包含日志时间。</li><li>标签是否写错。</li></ul><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl http://localhost:3100/ready</span><br></pre></td></tr></tbody></table></figure><h3 id="查询很慢"><a href="#查询很慢" class="headerlink" title="查询很慢"></a>查询很慢</h3><p>常见原因：</p><ul><li>时间范围太大。</li><li>标签过滤太少。</li><li>日志量太大。</li><li>查询中用了宽泛正则。</li><li>标签设计不合理。</li></ul><p>优化方向：</p><ul><li>先用标签缩小范围。</li><li>默认仪表盘时间不要太长。</li><li>对高频日志降噪。</li><li>避免把请求 ID 作为 label。</li></ul><h3 id="日志丢失"><a href="#日志丢失" class="headerlink" title="日志丢失"></a>日志丢失</h3><p>排查：</p><ul><li>采集器是否重启。</li><li>容器日志是否轮转。</li><li>Loki 存储是否满了。</li><li>retention 是否过短。</li><li>时间戳是否异常。</li></ul><h3 id="entry-too-far-behind"><a href="#entry-too-far-behind" class="headerlink" title="entry too far behind"></a><code>entry too far behind</code></h3><p>日志时间戳比当前时间落后太多，可能是：</p><ul><li>机器时间不准。</li><li>旧日志被重新采集。</li><li>应用写入了错误时间。</li></ul><p>处理方向：</p><ul><li>同步服务器时间。</li><li>调整采集位置。</li><li>清理旧 position 文件后谨慎重采。</li></ul><h2 id="Loki-与-ELK-怎么选"><a href="#Loki-与-ELK-怎么选" class="headerlink" title="Loki 与 ELK 怎么选"></a>Loki 与 ELK 怎么选</h2><div class="md-table-scroll"><table><thead><tr><th>对比</th><th>Loki</th><th>Elastic Stack</th></tr></thead><tbody><tr><td>查询入口</td><td>Grafana</td><td>Kibana</td></tr><tr><td>索引方式</td><td>标签索引为主</td><td>字段全文索引能力强</td></tr><tr><td>成本</td><td>相对低</td><td>相对高</td></tr><tr><td>查询能力</td><td>适合按标签找日志</td><td>适合复杂搜索分析</td></tr><tr><td>学习成本</td><td>较低</td><td>较高</td></tr></tbody></table></div><p>如果只是按服务、容器、等级、关键词查日志，Loki 很合适。如果需要大量字段检索、复杂聚合和安全审计分析，Elastic Stack 更合适。</p><h2 id="生产注意事项"><a href="#生产注意事项" class="headerlink" title="生产注意事项"></a>生产注意事项</h2><ul><li>标签数量和标签基数要严格控制。</li><li>设置日志保留时间。</li><li>日志采集器要监控自身状态。</li><li>多租户或多环境要分清 label。</li><li>敏感字段不要直接写入日志。</li><li>Grafana 权限要按团队划分。</li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/observability-loki/</id>
    <link href="https://blogs.microcyan.com/posts/observability-loki/"/>
    <published>2026-06-19T11:51:00.000Z</published>
    <summary>Grafana Loki 日志系统、Docker、LogQL、标签设计、日志采集、Grafana 数据源、Promtail、Alloy 和常见问题整理。</summary>
    <title>Loki 快速入门与常见问题</title>
    <updated>2026-09-08T10:34:43.299Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="Docker" scheme="https://blogs.microcyan.com/tags/Docker/"/>
    <category term="可观测性" scheme="https://blogs.microcyan.com/tags/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/"/>
    <category term="系统监控" scheme="https://blogs.microcyan.com/tags/%E7%B3%BB%E7%BB%9F%E7%9B%91%E6%8E%A7/"/>
    <category term="Elastic Stack" scheme="https://blogs.microcyan.com/tags/Elastic-Stack/"/>
    <content>
      <![CDATA[<!-- more --><p>ELK 通常指 <code>Elasticsearch + Logstash + Kibana</code>。现在更完整的名字是 Elastic Stack，常见组件还包括 Beats、Elastic Agent、APM 等。</p><blockquote><p><strong>官方入口</strong></p><ul><li><a target="_blank" rel="noopener" href="https://www.elastic.co/docs/get-started/the-stack">Elastic Stack 文档</a></li><li><a target="_blank" rel="noopener" href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/installing-elasticsearch">Elasticsearch 安装</a></li><li><a target="_blank" rel="noopener" href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-elasticsearch-docker-basic">Elasticsearch Docker 快速启动</a></li><li><a target="_blank" rel="noopener" href="https://www.elastic.co/docs/reference/logstash/getting-started-with-logstash">Logstash 入门</a></li><li><a target="_blank" rel="noopener" href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/install-kibana-with-docker">Kibana Docker 安装</a></li></ul></blockquote><h2 id="组件职责"><a href="#组件职责" class="headerlink" title="组件职责"></a>组件职责</h2><div class="md-table-scroll"><table><thead><tr><th>组件</th><th>作用</th></tr></thead><tbody><tr><td>Elasticsearch</td><td>存储、索引、搜索和聚合</td></tr><tr><td>Logstash</td><td>采集、解析、过滤、转换和输出日志</td></tr><tr><td>Kibana</td><td>查询、可视化、仪表盘和管理界面</td></tr><tr><td>Beats / Elastic Agent</td><td>轻量采集客户端</td></tr></tbody></table></div><p>日志链路通常是：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">应用日志 -&gt; Filebeat/Logstash -&gt; Elasticsearch -&gt; Kibana</span><br></pre></td></tr></tbody></table></figure><h2 id="版本注意"><a href="#版本注意" class="headerlink" title="版本注意"></a>版本注意</h2><p>Elastic Stack 各组件要保持同一大版本，最好同一具体版本。例如 Elasticsearch、Kibana、Logstash 都使用 <code>9.4.1</code>。混用版本容易出现连接、索引模板、认证和 API 兼容问题。</p><h2 id="本地快速体验"><a href="#本地快速体验" class="headerlink" title="本地快速体验"></a>本地快速体验</h2><p>开发测试可以先只启动 Elasticsearch 和 Kibana。生产环境需要考虑安全、内存、磁盘、快照、索引生命周期和集群高可用。</p><figure class="highlight yaml"><table><tbody><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="attr">services:</span></span><br><span class="line">  <span class="attr">elasticsearch:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">docker.elastic.co/elasticsearch/elasticsearch:9.4.1</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">elasticsearch</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">discovery.type=single-node</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">xpack.security.enabled=false</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">ES_JAVA_OPTS=-Xms1g</span> <span class="string">-Xmx1g</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">"9200:9200"</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">es_data:/usr/share/elasticsearch/data</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">kibana:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">docker.elastic.co/kibana/kibana:9.4.1</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">kibana</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">ELASTICSEARCH_HOSTS=http://elasticsearch:9200</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">"5601:5601"</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">elasticsearch</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">es_data:</span></span><br></pre></td></tr></tbody></table></figure><p>启动：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d</span><br><span class="line">curl http://localhost:9200</span><br></pre></td></tr></tbody></table></figure><p>Kibana：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:5601</span><br></pre></td></tr></tbody></table></figure><blockquote><p><strong>本地配置不要直接用于生产</strong>上面的示例关闭了安全认证，只适合本地学习。生产环境必须开启安全认证、TLS、权限控制和备份。</p></blockquote><h2 id="写入一条测试数据"><a href="#写入一条测试数据" class="headerlink" title="写入一条测试数据"></a>写入一条测试数据</h2><figure class="highlight bash"><table><tbody><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">curl -X POST <span class="string">"http://localhost:9200/app-logs/_doc"</span> \</span><br><span class="line">  -H <span class="string">"Content-Type: application/json"</span> \</span><br><span class="line">  -d <span class="string">'{</span></span><br><span class="line"><span class="string">    "@timestamp": "2026-05-28T12:00:00Z",</span></span><br><span class="line"><span class="string">    "level": "error",</span></span><br><span class="line"><span class="string">    "service": "api",</span></span><br><span class="line"><span class="string">    "message": "database connection timeout"</span></span><br><span class="line"><span class="string">  }'</span></span><br></pre></td></tr></tbody></table></figure><p>查询：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl <span class="string">"http://localhost:9200/app-logs/_search?pretty"</span></span><br></pre></td></tr></tbody></table></figure><h2 id="Logstash-最小配置"><a href="#Logstash-最小配置" class="headerlink" title="Logstash 最小配置"></a>Logstash 最小配置</h2><p><code>logstash.conf</code>：</p><figure class="highlight text"><table><tbody><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">input {</span><br><span class="line">  file {</span><br><span class="line">    path =&gt; "/logs/app.log"</span><br><span class="line">    start_position =&gt; "beginning"</span><br><span class="line">  }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">filter {</span><br><span class="line">  grok {</span><br><span class="line">    match =&gt; {</span><br><span class="line">      "message" =&gt; "%{TIMESTAMP_ISO8601:time} %{LOGLEVEL:level} %{GREEDYDATA:content}"</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">output {</span><br><span class="line">  elasticsearch {</span><br><span class="line">    hosts =&gt; ["http://elasticsearch:9200"]</span><br><span class="line">    index =&gt; "app-logs-%{+YYYY.MM.dd}"</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>Logstash 更适合需要解析和转换的场景。如果只是采集文件日志，Filebeat 或 Elastic Agent 通常更轻。</p><h2 id="索引和分片"><a href="#索引和分片" class="headerlink" title="索引和分片"></a>索引和分片</h2><div class="md-table-scroll"><table><thead><tr><th>概念</th><th>说明</th></tr></thead><tbody><tr><td>Index</td><td>类似一类文档集合</td></tr><tr><td>Document</td><td>一条 JSON 文档</td></tr><tr><td>Field</td><td>文档字段</td></tr><tr><td>Mapping</td><td>字段类型和索引规则</td></tr><tr><td>Shard</td><td>分片，影响存储和查询</td></tr><tr><td>Replica</td><td>副本，影响可用性</td></tr></tbody></table></div><p>常见日志索引命名：</p><figure class="highlight text"><table><tbody><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">app-logs-2026.05.28</span><br><span class="line">nginx-access-2026.05.28</span><br><span class="line">payment-error-2026.05.28</span><br></pre></td></tr></tbody></table></figure><p>数据量不大时，不要给每个小业务拆太多索引，否则 shard 数会膨胀。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="vm-max-map-count-is-too-low"><a href="#vm-max-map-count-is-too-low" class="headerlink" title="vm.max_map_count is too low"></a><code>vm.max_map_count is too low</code></h3><p>Linux 上 Elasticsearch 常见报错：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> sysctl -w vm.max_map_count=262144</span><br></pre></td></tr></tbody></table></figure><p>长期生效：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">"vm.max_map_count=262144"</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.conf</span><br><span class="line"><span class="built_in">sudo</span> sysctl -p</span><br></pre></td></tr></tbody></table></figure><h3 id="Kibana-连不上-Elasticsearch"><a href="#Kibana-连不上-Elasticsearch" class="headerlink" title="Kibana 连不上 Elasticsearch"></a>Kibana 连不上 Elasticsearch</h3><p>排查：</p><ul><li><code>ELASTICSEARCH_HOSTS</code> 是否写错。</li><li>容器网络里是否应该写服务名 <code>elasticsearch</code>，而不是 <code>localhost</code>。</li><li>Elasticsearch 是否启动完成。</li><li>安全认证是否开启。</li><li>版本是否一致。</li></ul><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker logs elasticsearch</span><br><span class="line">docker logs kibana</span><br><span class="line">curl http://localhost:9200</span><br></pre></td></tr></tbody></table></figure><h3 id="Elasticsearch-启动后很快退出"><a href="#Elasticsearch-启动后很快退出" class="headerlink" title="Elasticsearch 启动后很快退出"></a>Elasticsearch 启动后很快退出</h3><p>常见原因：</p><ul><li>内存太小。</li><li>数据目录权限错误。</li><li><code>vm.max_map_count</code> 不满足。</li><li>配置了生产网络但 bootstrap checks 不通过。</li><li>磁盘空间不足。</li></ul><h3 id="日志进来了但-Kibana-搜不到"><a href="#日志进来了但-Kibana-搜不到" class="headerlink" title="日志进来了但 Kibana 搜不到"></a>日志进来了但 Kibana 搜不到</h3><p>检查：</p><ul><li>时间字段是否正确。</li><li>Kibana 时间范围是否包含数据时间。</li><li>index pattern 或 data view 是否匹配。</li><li>字段类型是否被错误映射。</li><li>Logstash 输出 index 名称是否符合预期。</li></ul><h3 id="磁盘被打满"><a href="#磁盘被打满" class="headerlink" title="磁盘被打满"></a>磁盘被打满</h3><p>处理方向：</p><ul><li>配置索引生命周期。</li><li>限制日志保留天数。</li><li>避免 debug 日志长期打开。</li><li>不要把大字段、请求体、响应体全量写入。</li><li>配置快照后删除过期索引。</li></ul><h2 id="生产注意事项"><a href="#生产注意事项" class="headerlink" title="生产注意事项"></a>生产注意事项</h2><ul><li>Elasticsearch、Kibana、Logstash 保持同版本。</li><li>生产环境开启认证和 TLS。</li><li>设置 JVM heap，不要让 ES 抢完整机内存。</li><li>控制 shard 数量。</li><li>建立快照策略。</li><li>日志字段先规范，再进入大规模采集。</li><li>对敏感字段脱敏，例如手机号、token、身份证、支付信息。</li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/observability-elastic-stack/</id>
    <link href="https://blogs.microcyan.com/posts/observability-elastic-stack/"/>
    <published>2026-06-12T00:31:00.000Z</published>
    <summary>Elastic Stack、ELK、Elasticsearch、Logstash、Kibana、索引、日志采集、Docker、搜索分析和常见报错整理。</summary>
    <title>Elastic Stack / ELK 快速入门与常见问题</title>
    <updated>2026-09-08T10:34:43.299Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="InfluxDB" scheme="https://blogs.microcyan.com/tags/InfluxDB/"/>
    <category term="Grafana" scheme="https://blogs.microcyan.com/tags/Grafana/"/>
    <category term="Docker" scheme="https://blogs.microcyan.com/tags/Docker/"/>
    <category term="Loki" scheme="https://blogs.microcyan.com/tags/Loki/"/>
    <category term="可观测性" scheme="https://blogs.microcyan.com/tags/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/"/>
    <content>
      <![CDATA[<!-- more --><p>Grafana 是常用的可视化和告警平台，本身不负责长期存储数据，主要连接 InfluxDB、Prometheus、Loki、Elasticsearch、MySQL 等数据源，然后做仪表盘、查询和告警。</p><blockquote><p><strong>官方入口</strong></p><ul><li><a target="_blank" rel="noopener" href="https://grafana.com/docs/grafana/latest/">Grafana 文档</a></li><li><a target="_blank" rel="noopener" href="https://grafana.com/docs/grafana/latest/setup-grafana/installation/docker/">Grafana Docker 安装</a></li><li><a target="_blank" rel="noopener" href="https://grafana.com/docs/grafana/latest/fundamentals/getting-started/first-dashboards/get-started-grafana-prometheus/">Grafana + Prometheus 入门</a></li></ul></blockquote><h2 id="Docker-快速启动"><a href="#Docker-快速启动" class="headerlink" title="Docker 快速启动"></a>Docker 快速启动</h2><figure class="highlight bash"><table><tbody><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">docker volume create grafana-storage</span><br><span class="line"></span><br><span class="line">docker run -d \</span><br><span class="line">  -p 3000:3000 \</span><br><span class="line">  --name grafana \</span><br><span class="line">  --volume grafana-storage:/var/lib/grafana \</span><br><span class="line">  grafana/grafana-enterprise</span><br></pre></td></tr></tbody></table></figure><p>访问：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:3000</span><br></pre></td></tr></tbody></table></figure><p>默认账号通常是：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">admin / admin</span><br></pre></td></tr></tbody></table></figure><p>首次登录后需要修改密码。</p><h2 id="Docker-Compose"><a href="#Docker-Compose" class="headerlink" title="Docker Compose"></a>Docker Compose</h2><figure class="highlight yaml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">grafana:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">grafana/grafana-enterprise</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">grafana</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">"3000:3000"</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">grafana_storage:/var/lib/grafana</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">GF_SECURITY_ADMIN_USER:</span> <span class="string">admin</span></span><br><span class="line">      <span class="attr">GF_SECURITY_ADMIN_PASSWORD:</span> <span class="string">change_me</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">grafana_storage:</span></span><br></pre></td></tr></tbody></table></figure><p>启动：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d</span><br></pre></td></tr></tbody></table></figure><h2 id="常见数据源"><a href="#常见数据源" class="headerlink" title="常见数据源"></a>常见数据源</h2><div class="md-table-scroll"><table><thead><tr><th>数据源</th><th>适合内容</th></tr></thead><tbody><tr><td>Prometheus</td><td>服务器指标、应用指标、Kubernetes 指标</td></tr><tr><td>InfluxDB</td><td>IoT、传感器、业务指标、时序数据</td></tr><tr><td>Loki</td><td>日志查询</td></tr><tr><td>Elasticsearch</td><td>日志搜索、事件分析</td></tr><tr><td>MySQL</td><td>业务报表、管理后台统计</td></tr></tbody></table></div><h2 id="添加-InfluxDB-数据源"><a href="#添加-InfluxDB-数据源" class="headerlink" title="添加 InfluxDB 数据源"></a>添加 InfluxDB 数据源</h2><p>重点配置：</p><div class="md-table-scroll"><table><thead><tr><th>项</th><th>示例</th></tr></thead><tbody><tr><td>URL</td><td><code>http://influxdb:8086</code></td></tr><tr><td>Organization</td><td><code>leroi</code></td></tr><tr><td>Token</td><td><code>change_me_token</code></td></tr><tr><td>Bucket</td><td><code>metrics</code></td></tr></tbody></table></div><p>如果 Grafana 和 InfluxDB 在同一个 Docker Compose 网络里，URL 应该写服务名，不是 <code>localhost</code>。</p><h2 id="添加-Loki-数据源"><a href="#添加-Loki-数据源" class="headerlink" title="添加 Loki 数据源"></a>添加 Loki 数据源</h2><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://loki:3100</span><br></pre></td></tr></tbody></table></figure><p>Grafana 中使用 LogQL 查询：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{job="nginx"}</span><br></pre></td></tr></tbody></table></figure><p>按关键词过滤：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{job="nginx"} |= "error"</span><br></pre></td></tr></tbody></table></figure><h2 id="面板设计建议"><a href="#面板设计建议" class="headerlink" title="面板设计建议"></a>面板设计建议</h2><ul><li>一个仪表盘只解决一个主题，不要所有指标塞一页。</li><li>关键指标放上方，比如状态、错误数、请求量、耗时。</li><li>图表标题写清楚单位。</li><li>时间范围默认不要太大，避免每次打开都扫全量数据。</li><li>变量用于环境、服务、主机、设备切换。</li><li>告警规则要有恢复条件和通知分组。</li></ul><h2 id="反向代理配置"><a href="#反向代理配置" class="headerlink" title="反向代理配置"></a>反向代理配置</h2><p>Nginx 示例：</p><figure class="highlight nginx"><table><tbody><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="section">location</span> / {</span><br><span class="line">  <span class="attribute">proxy_pass</span> http://127.0.0.1:3000;</span><br><span class="line">  <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">  <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">  <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">  <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>如果 Grafana 放在子路径，例如 <code>/grafana/</code>，需要配置：</p><figure class="highlight ini"><table><tbody><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="section">[server]</span></span><br><span class="line"><span class="attr">root_url</span> = https://example.com/grafana/</span><br><span class="line"><span class="attr">serve_from_sub_path</span> = <span class="literal">true</span></span><br></pre></td></tr></tbody></table></figure><p>Docker 环境变量写法：</p><figure class="highlight yaml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">environment:</span></span><br><span class="line">  <span class="attr">GF_SERVER_ROOT_URL:</span> <span class="string">https://example.com/grafana/</span></span><br><span class="line">  <span class="attr">GF_SERVER_SERVE_FROM_SUB_PATH:</span> <span class="string">"true"</span></span><br></pre></td></tr></tbody></table></figure><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="登录后数据全没了"><a href="#登录后数据全没了" class="headerlink" title="登录后数据全没了"></a>登录后数据全没了</h3><p>容器没有挂载 <code>/var/lib/grafana</code>，删除容器后 SQLite 数据库也没了。使用 volume：</p><figure class="highlight yaml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">grafana_storage:/var/lib/grafana</span></span><br></pre></td></tr></tbody></table></figure><h3 id="数据源测试失败"><a href="#数据源测试失败" class="headerlink" title="数据源测试失败"></a>数据源测试失败</h3><p>排查：</p><ul><li>Grafana 容器里是否能访问数据源。</li><li>URL 是否写成了错误的 <code>localhost</code>。</li><li>token、账号、密码是否正确。</li><li>数据源服务是否监听在容器网络可访问地址。</li><li>防火墙是否拦截。</li></ul><h3 id="反向代理后跳转地址不对"><a href="#反向代理后跳转地址不对" class="headerlink" title="反向代理后跳转地址不对"></a>反向代理后跳转地址不对</h3><p>检查：</p><ul><li><code>root_url</code>。</li><li><code>serve_from_sub_path</code>。</li><li>代理是否传了 <code>X-Forwarded-Proto</code>。</li><li>域名 HTTPS 和内部 HTTP 的协议判断是否一致。</li></ul><h3 id="面板一直-No-data"><a href="#面板一直-No-data" class="headerlink" title="面板一直 No data"></a>面板一直 No data</h3><p>常见原因：</p><ul><li>时间范围不包含数据。</li><li>查询条件过窄。</li><li>数据源变量为空。</li><li>字段单位或聚合函数选错。</li><li>数据写入到了另一个库、bucket 或 index。</li></ul><h3 id="告警没有通知"><a href="#告警没有通知" class="headerlink" title="告警没有通知"></a>告警没有通知</h3><p>检查：</p><ul><li>Alert rule 是否处于 firing。</li><li>Contact point 是否配置。</li><li>Notification policy 是否匹配。</li><li>静默规则是否生效。</li><li>查询是否依赖面板变量。</li></ul><h2 id="生产注意事项"><a href="#生产注意事项" class="headerlink" title="生产注意事项"></a>生产注意事项</h2><ul><li>管理员密码不要用默认值。</li><li>给普通用户只读权限。</li><li>数据源 token 最小权限。</li><li>定期导出重要 dashboard JSON。</li><li>使用持久化存储。</li><li>对公网开放时加 HTTPS、SSO、IP 限制或反向代理认证。</li><li>插件不要随意安装来源不明版本。</li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/observability-grafana/</id>
    <link href="https://blogs.microcyan.com/posts/observability-grafana/"/>
    <published>2026-06-09T07:47:00.000Z</published>
    <summary>Grafana 仪表盘、数据源、Docker、InfluxDB、Prometheus、Loki、面板、变量、告警、反向代理和常见问题整理。</summary>
    <title>Grafana 快速入门与常见问题</title>
    <updated>2026-09-08T10:34:43.299Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="InfluxDB" scheme="https://blogs.microcyan.com/tags/InfluxDB/"/>
    <category term="Grafana" scheme="https://blogs.microcyan.com/tags/Grafana/"/>
    <category term="Docker" scheme="https://blogs.microcyan.com/tags/Docker/"/>
    <category term="可观测性" scheme="https://blogs.microcyan.com/tags/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/"/>
    <category term="系统监控" scheme="https://blogs.microcyan.com/tags/%E7%B3%BB%E7%BB%9F%E7%9B%91%E6%8E%A7/"/>
    <content>
      <![CDATA[<!-- more --><p>InfluxDB 是时序数据库，适合存储随时间持续变化的数据，比如设备温度、电流、接口耗时、服务器 CPU、网络流量、传感器数据和业务指标。</p><blockquote><p><strong>官方入口</strong></p><ul><li><a target="_blank" rel="noopener" href="https://docs.influxdata.com/influxdb3/core/">InfluxDB 3 Core 文档</a></li><li><a target="_blank" rel="noopener" href="https://docs.influxdata.com/influxdb3/core/install/">InfluxDB 3 Core 安装</a></li><li><a target="_blank" rel="noopener" href="https://docs.influxdata.com/influxdb/v2/">InfluxDB 2.x 文档</a></li><li><a target="_blank" rel="noopener" href="https://hub.docker.com/_/influxdb">InfluxDB Docker 镜像</a></li></ul></blockquote><h2 id="版本怎么选"><a href="#版本怎么选" class="headerlink" title="版本怎么选"></a>版本怎么选</h2><div class="md-table-scroll"><table><thead><tr><th>版本</th><th>适合场景</th></tr></thead><tbody><tr><td>InfluxDB 3 Core</td><td>新项目、实时监控、希望使用 3.x 架构</td></tr><tr><td>InfluxDB 2.x</td><td>现有项目、Grafana/Telegraf 生态、Bucket/Token/Flux 使用较多</td></tr><tr><td>InfluxDB 1.x</td><td>历史项目维护，不建议新项目从 1.x 开始</td></tr></tbody></table></div><blockquote><p><strong>注意 Docker 标签</strong>从 <code>2026-05-27</code> 开始，<code>influxdb:latest</code> 会指向 InfluxDB 3 Core。生产环境不要使用 <code>latest</code>，请写明确版本，例如 <code>influxdb:2.9</code>、<code>influxdb:3-core</code>。</p></blockquote><h2 id="核心概念"><a href="#核心概念" class="headerlink" title="核心概念"></a>核心概念</h2><p>InfluxDB 2.x 常见概念：</p><div class="md-table-scroll"><table><thead><tr><th>概念</th><th>说明</th></tr></thead><tbody><tr><td>Organization</td><td>组织</td></tr><tr><td>Bucket</td><td>数据桶，类似数据库加保留策略</td></tr><tr><td>Token</td><td>API 鉴权令牌</td></tr><tr><td>Measurement</td><td>指标集合，例如 <code>temperature</code></td></tr><tr><td>Tag</td><td>标签，适合做过滤条件，例如 <code>device_id</code>、<code>room</code></td></tr><tr><td>Field</td><td>字段，实际数值，例如 <code>value=25.6</code></td></tr><tr><td>Timestamp</td><td>时间戳</td></tr></tbody></table></div><p>InfluxDB 3 Core 中更常见的说法是 database、table、column，但写入 Line Protocol 的思想仍然很接近。</p><h2 id="InfluxDB-2-x-Docker-Compose"><a href="#InfluxDB-2-x-Docker-Compose" class="headerlink" title="InfluxDB 2.x Docker Compose"></a>InfluxDB 2.x Docker Compose</h2><figure class="highlight yaml"><table><tbody><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="attr">services:</span></span><br><span class="line">  <span class="attr">influxdb:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">influxdb:2.9</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">influxdb</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">"8086:8086"</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">DOCKER_INFLUXDB_INIT_MODE:</span> <span class="string">setup</span></span><br><span class="line">      <span class="attr">DOCKER_INFLUXDB_INIT_USERNAME:</span> <span class="string">admin</span></span><br><span class="line">      <span class="attr">DOCKER_INFLUXDB_INIT_PASSWORD:</span> <span class="string">admin_password_change_me</span></span><br><span class="line">      <span class="attr">DOCKER_INFLUXDB_INIT_ORG:</span> <span class="string">leroi</span></span><br><span class="line">      <span class="attr">DOCKER_INFLUXDB_INIT_BUCKET:</span> <span class="string">metrics</span></span><br><span class="line">      <span class="attr">DOCKER_INFLUXDB_INIT_ADMIN_TOKEN:</span> <span class="string">change_me_token</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">influxdb_data:/var/lib/influxdb2</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">influxdb_config:/etc/influxdb2</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">influxdb_data:</span></span><br><span class="line">  <span class="attr">influxdb_config:</span></span><br></pre></td></tr></tbody></table></figure><p>启动：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d</span><br><span class="line">docker logs -f influxdb</span><br></pre></td></tr></tbody></table></figure><p>访问：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:8086</span><br></pre></td></tr></tbody></table></figure><h2 id="写入-Line-Protocol"><a href="#写入-Line-Protocol" class="headerlink" title="写入 Line Protocol"></a>写入 Line Protocol</h2><p>Line Protocol 基本格式：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">measurement,tag_key=tag_value field_key=field_value timestamp</span><br></pre></td></tr></tbody></table></figure><p>例子：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">temperature,device=esp32,room=office value=26.3</span><br></pre></td></tr></tbody></table></figure><p>通过 HTTP 写入 InfluxDB 2.x：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">curl --request POST <span class="string">"http://localhost:8086/api/v2/write?org=leroi&amp;bucket=metrics&amp;precision=s"</span> \</span><br><span class="line">  --header <span class="string">"Authorization: Token change_me_token"</span> \</span><br><span class="line">  --data-raw <span class="string">"temperature,device=esp32,room=office value=26.3"</span></span><br></pre></td></tr></tbody></table></figure><h2 id="Flux-查询"><a href="#Flux-查询" class="headerlink" title="Flux 查询"></a>Flux 查询</h2><figure class="highlight text"><table><tbody><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">from(bucket: "metrics")</span><br><span class="line">  |&gt; range(start: -1h)</span><br><span class="line">  |&gt; filter(fn: (r) =&gt; r._measurement == "temperature")</span><br><span class="line">  |&gt; filter(fn: (r) =&gt; r.device == "esp32")</span><br></pre></td></tr></tbody></table></figure><p>常见聚合：</p><figure class="highlight text"><table><tbody><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">from(bucket: "metrics")</span><br><span class="line">  |&gt; range(start: -24h)</span><br><span class="line">  |&gt; filter(fn: (r) =&gt; r._measurement == "temperature")</span><br><span class="line">  |&gt; aggregateWindow(every: 5m, fn: mean, createEmpty: false)</span><br></pre></td></tr></tbody></table></figure><h2 id="Grafana-连接-InfluxDB"><a href="#Grafana-连接-InfluxDB" class="headerlink" title="Grafana 连接 InfluxDB"></a>Grafana 连接 InfluxDB</h2><p>Grafana 添加数据源时重点看：</p><div class="md-table-scroll"><table><thead><tr><th>配置</th><th>说明</th></tr></thead><tbody><tr><td>URL</td><td><code>http://influxdb:8086</code> 或公网地址</td></tr><tr><td>Organization</td><td>初始化时设置的 org</td></tr><tr><td>Token</td><td>有读权限的 token</td></tr><tr><td>Bucket</td><td>要查询的数据桶</td></tr><tr><td>Query language</td><td>2.x 常见 Flux，旧项目可能用 InfluxQL</td></tr></tbody></table></div><p>Docker Compose 同网络时，Grafana 里不要写 <code>localhost:8086</code>，应写服务名，例如 <code>http://influxdb:8086</code>。</p><h2 id="Schema-设计注意事项"><a href="#Schema-设计注意事项" class="headerlink" title="Schema 设计注意事项"></a>Schema 设计注意事项</h2><h3 id="Tag-不要乱放"><a href="#Tag-不要乱放" class="headerlink" title="Tag 不要乱放"></a>Tag 不要乱放</h3><p>适合作为 tag：</p><ul><li>设备 ID。</li><li>区域。</li><li>主机名。</li><li>环境名。</li><li>状态类型。</li></ul><p>不适合作为 tag：</p><ul><li>用户 ID 数量极大。</li><li>请求 ID。</li><li>时间戳。</li><li>随机字符串。</li><li>订单号。</li></ul><p>高基数 tag 会让查询和存储压力变大。</p><h3 id="Field-放实际数值"><a href="#Field-放实际数值" class="headerlink" title="Field 放实际数值"></a>Field 放实际数值</h3><p>例如：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">temperature,device=esp32 value=26.3</span><br><span class="line">cpu,host=server01 usage=73.2</span><br></pre></td></tr></tbody></table></figure><h3 id="保留策略要提前想"><a href="#保留策略要提前想" class="headerlink" title="保留策略要提前想"></a>保留策略要提前想</h3><p>不要无限保存所有原始数据。常见做法：</p><ul><li>原始数据保存 7 天或 30 天。</li><li>5 分钟聚合数据保存 3 到 12 个月。</li><li>长期报表只保存小时级或天级统计。</li></ul><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="访问-8086-打不开"><a href="#访问-8086-打不开" class="headerlink" title="访问 8086 打不开"></a>访问 <code>8086</code> 打不开</h3><p>排查：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker ps</span><br><span class="line">docker logs influxdb</span><br><span class="line">curl http://localhost:8086/health</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li>容器没有启动。</li><li>端口映射不对。</li><li>初始化密码不满足要求。</li><li>数据卷里已有旧初始化状态，环境变量不再生效。</li></ul><h3 id="Token-认证失败"><a href="#Token-认证失败" class="headerlink" title="Token 认证失败"></a>Token 认证失败</h3><p>确认：</p><ul><li>Header 是否写成 <code>Authorization: Token xxx</code>。</li><li>Token 是否有对应 bucket 的读写权限。</li><li>org、bucket 名称是否完全一致。</li><li>复制 token 时是否多了空格或换行。</li></ul><h3 id="写入成功但查询不到"><a href="#写入成功但查询不到" class="headerlink" title="写入成功但查询不到"></a>写入成功但查询不到</h3><p>常见原因：</p><ul><li>查询时间范围不包含写入时间。</li><li>bucket 或 org 写错。</li><li>measurement 名称拼写不一致。</li><li>时间戳精度不匹配。</li><li>写入到了另一个环境。</li></ul><p>先用最近时间查：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">from(bucket: "metrics")</span><br><span class="line">  |&gt; range(start: -24h)</span><br></pre></td></tr></tbody></table></figure><h3 id="数据量越来越大"><a href="#数据量越来越大" class="headerlink" title="数据量越来越大"></a>数据量越来越大</h3><p>处理方向：</p><ul><li>设置 bucket retention。</li><li>用 task 做降采样。</li><li>控制采集频率。</li><li>避免高基数 tag。</li><li>删除不必要字段。</li></ul><h3 id="升级后-token-看不到明文"><a href="#升级后-token-看不到明文" class="headerlink" title="升级后 token 看不到明文"></a>升级后 token 看不到明文</h3><p>InfluxDB 2.9 起 token 会更强调哈希存储安全。实际使用中，token 只应该在创建时保存到密码管理器或部署变量里，不要依赖后台页面再次查看明文。</p><h2 id="生产注意事项"><a href="#生产注意事项" class="headerlink" title="生产注意事项"></a>生产注意事项</h2><ul><li>不要使用 <code>latest</code> 镜像标签。</li><li>token 不要写进前端代码。</li><li>给写入、查询、管理分别创建不同 token。</li><li>给 bucket 设置保留时间。</li><li>重要数据定期备份。</li><li>Grafana 查询面板不要默认扫全量历史数据。</li><li>设备上报要做重试和限流，避免断网恢复后瞬间打爆服务。</li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/observability-influxdb/</id>
    <link href="https://blogs.microcyan.com/posts/observability-influxdb/"/>
    <published>2026-06-08T12:04:00.000Z</published>
    <summary>InfluxDB 3 Core、InfluxDB 2.x、时序数据库、Docker、Line Protocol、Bucket、Token、Flux、SQL、Grafana 和常见问题。</summary>
    <title>InfluxDB 快速入门与常见问题</title>
    <updated>2026-09-08T10:34:43.299Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="后端开发" scheme="https://blogs.microcyan.com/categories/backend/"/>
    <category term="PHP" scheme="https://blogs.microcyan.com/tags/PHP/"/>
    <category term="后端开发" scheme="https://blogs.microcyan.com/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <content>
      <![CDATA[<!-- more --><p>这篇整理 PHP 从 5.0 到 8.5 的主要变化。它更适合在维护老项目、判断升级风险、阅读旧代码时快速对照。</p><p>截至 2026-05-29，PHP 官方仍在支持的分支是 <code>8.2</code>、<code>8.3</code>、<code>8.4</code>、<code>8.5</code>。其中 <code>8.5</code> 是当前最新稳定分支。PHP 5、PHP 7.0 到 7.4、PHP 8.0 到 8.1 都已经不适合作为新项目运行环境。</p><h2 id="版本速览"><a href="#版本速览" class="headerlink" title="版本速览"></a>版本速览</h2><div class="md-table-scroll"><table><thead><tr><th>版本</th><th>核心变化</th><th>维护时要注意</th></tr></thead><tbody><tr><td>PHP 5.0</td><td>Zend Engine 2、对象模型重写、访问控制、接口、抽象类、异常、构造析构方法</td><td>很老的项目可能还保留 PHP 4 风格构造函数</td></tr><tr><td>PHP 5.1</td><td>PDO 默认启用、日期时间处理重写、性能提升</td><td>数据库访问开始可以统一走 PDO</td></tr><tr><td>PHP 5.2</td><td>JSON、Filter、Zip、DateTime、内存管理改进</td><td><code>json_encode()</code>、<code>filter_input()</code> 开始更常见</td></tr><tr><td>PHP 5.3</td><td>命名空间、闭包、后期静态绑定、nowdoc、三元简写、<code>__callStatic()</code></td><td>现代框架雏形开始出现</td></tr><tr><td>PHP 5.4</td><td>短数组、Trait、内置 Web Server、数组解引用、移除 <code>register_globals</code>、<code>magic_quotes</code>、<code>safe_mode</code></td><td>老代码里依赖魔术引号会出问题</td></tr><tr><td>PHP 5.5</td><td>Generator、<code>finally</code>、<code>password_hash()</code>、<code>ClassName::class</code>、<code>empty()</code> 支持表达式、OPcache</td><td>密码存储应逐步迁移到 <code>password_hash()</code></td></tr><tr><td>PHP 5.6</td><td>可变参数、参数解包、常量表达式、函数与常量导入、<code>hash_equals()</code>、<code>phpdbg</code></td><td>HTTPS 证书校验更严格，旧接口可能暴露证书问题</td></tr><tr><td>PHP 7.0</td><td>标量类型、返回值类型、<code>??</code>、<code>&lt;=&gt;</code>、匿名类、<code>Throwable</code>、性能大幅提升</td><td><code>mysql_*</code> 扩展被移除，要迁移到 PDO 或 MySQLi</td></tr><tr><td>PHP 7.1</td><td>可空类型、<code>void</code>、<code>iterable</code>、多异常捕获、数组解构增强</td><td>接口签名开始更严格</td></tr><tr><td>PHP 7.2</td><td><code>object</code> 类型、Sodium 成为核心扩展、参数类型放宽</td><td><code>count()</code> 非数组参数会出现警告</td></tr><tr><td>PHP 7.3</td><td>heredoc/nowdoc 语法更灵活、函数调用尾逗号、<code>is_countable()</code>、<code>array_key_first()</code></td><td>兼容旧代码时可以先用 <code>is_countable()</code> 包一层</td></tr><tr><td>PHP 7.4</td><td>类型属性、箭头函数、<code>??=</code>、数组展开、OPcache 预加载、弱引用</td><td>属性必须先初始化，未初始化就读取会报错</td></tr><tr><td>PHP 8.0</td><td>命名参数、Attribute、构造器属性提升、联合类型、<code>match</code>、空安全操作符、JIT</td><td>许多 warning 变成异常，老代码要重点测错误处理</td></tr><tr><td>PHP 8.1</td><td>Enum、只读属性、Fiber、交叉类型、一等 callable、<code>never</code></td><td>枚举适合替换一堆状态常量</td></tr><tr><td>PHP 8.2</td><td>只读类、DNF 类型、<code>true/false/null</code> 独立类型、动态属性弃用</td><td>动态给对象塞属性会触发弃用警告</td></tr><tr><td>PHP 8.3</td><td>类型化类常量、<code>#[Override]</code>、动态类常量获取、<code>json_validate()</code>、只读属性克隆改进</td><td>适合加强继承检查和 JSON 校验</td></tr><tr><td>PHP 8.4</td><td>属性钩子、非对称属性可见性、更新后的 DOM API、Lazy Object、<code>new</code> 后可直接链式调用</td><td>DTO、值对象、ORM 懒加载会更好写</td></tr><tr><td>PHP 8.5</td><td>URI 扩展、管道操作符、<code>clone()</code> 修改属性、<code>#[NoDiscard]</code>、常量表达式支持闭包、一批数组和 cURL 改进</td><td>新语法很方便，但要确认服务器、框架和扩展是否跟上</td></tr></tbody></table></div><h2 id="PHP-5：现代-PHP-的起点"><a href="#PHP-5：现代-PHP-的起点" class="headerlink" title="PHP 5：现代 PHP 的起点"></a>PHP 5：现代 PHP 的起点</h2><p>PHP 5 的重点是对象模型升级。今天的类、接口、抽象类、异常处理，大量基础都从这个阶段稳定下来。</p><h3 id="PHP-5-0：对象模型变化"><a href="#PHP-5-0：对象模型变化" class="headerlink" title="PHP 5.0：对象模型变化"></a>PHP 5.0：对象模型变化</h3><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Logger</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">info</span>(<span class="params"><span class="variable">$message</span></span>)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">Service</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$logger</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params">Logger <span class="variable">$logger</span></span>)</span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">        <span class="variable language_">$this</span>-&gt;logger = <span class="variable">$logger</span>;</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">abstract</span> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">handle</span>(<span class="params"></span>)</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>这类写法在今天看起来很普通，但在 PHP 5 之前，对象模型没有这么完整。维护非常老的代码时，可能会看到这种 PHP 4 风格构造函数：</p><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserService</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">UserService</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">        <span class="comment">// 老式构造函数，不建议继续使用</span></span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>新代码统一写成 <code>__construct()</code>。</p><h3 id="PHP-5-1-到-5-2：PDO、JSON、过滤器"><a href="#PHP-5-1-到-5-2：PDO、JSON、过滤器" class="headerlink" title="PHP 5.1 到 5.2：PDO、JSON、过滤器"></a>PHP 5.1 到 5.2：PDO、JSON、过滤器</h3><p>PDO 让数据库访问更统一，JSON 扩展成为接口开发里的基础能力。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$pdo</span> = <span class="keyword">new</span> <span class="title function_ invoke__">PDO</span>(</span><br><span class="line">    <span class="string">'mysql:host=127.0.0.1;dbname=demo;charset=utf8mb4'</span>,</span><br><span class="line">    <span class="string">'root'</span>,</span><br><span class="line">    <span class="string">'secret'</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="variable">$stmt</span> = <span class="variable">$pdo</span>-&gt;<span class="title function_ invoke__">prepare</span>(<span class="string">'select id, name from users where id = ?'</span>);</span><br><span class="line"><span class="variable">$stmt</span>-&gt;<span class="title function_ invoke__">execute</span>([<span class="number">1</span>]);</span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">json_encode</span>(<span class="variable">$stmt</span>-&gt;<span class="title function_ invoke__">fetch</span>(PDO::<span class="variable constant_">FETCH_ASSOC</span>), JSON_UNESCAPED_UNICODE);</span><br></pre></td></tr></tbody></table></figure><p>输入过滤可以先做基础清洗，但不要把它当成完整的业务校验。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$email</span> = <span class="title function_ invoke__">filter_input</span>(INPUT_POST, <span class="string">'email'</span>, FILTER_VALIDATE_EMAIL);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (<span class="variable">$email</span> === <span class="literal">false</span> || <span class="variable">$email</span> === <span class="literal">null</span>) {</span><br><span class="line">    <span class="keyword">exit</span>(<span class="string">'邮箱格式不正确'</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-5-3：命名空间与闭包"><a href="#PHP-5-3：命名空间与闭包" class="headerlink" title="PHP 5.3：命名空间与闭包"></a>PHP 5.3：命名空间与闭包</h3><p>命名空间解决类名冲突，闭包让回调和集合处理更自然。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Service</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">OrderService</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">paidOrders</span>(<span class="params"><span class="keyword">array</span> <span class="variable">$orders</span></span>)</span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">        <span class="keyword">return</span> <span class="title function_ invoke__">array_filter</span>(<span class="variable">$orders</span>, function (<span class="keyword">array</span> <span class="variable">$order</span>) {</span><br><span class="line">            <span class="keyword">return</span> <span class="variable">$order</span>[<span class="string">'status'</span>] === <span class="string">'paid'</span>;</span><br><span class="line">        });</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>后期静态绑定让继承里的静态调用更符合直觉。</p><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Model</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">static</span> <span class="function"><span class="keyword">function</span> <span class="title">table</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">        <span class="keyword">return</span> <span class="title function_ invoke__">get_called_class</span>();</span><br><span class="line">    }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">User</span> <span class="keyword">extends</span> <span class="title">Model</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="title class_">User</span>::<span class="title function_ invoke__">table</span>();</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-5-4：短数组与-Trait"><a href="#PHP-5-4：短数组与-Trait" class="headerlink" title="PHP 5.4：短数组与 Trait"></a>PHP 5.4：短数组与 Trait</h3><p>短数组让配置和数据结构更清爽。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$config</span> = [</span><br><span class="line">    <span class="string">'debug'</span> =&gt; <span class="literal">true</span>,</span><br><span class="line">    <span class="string">'timezone'</span> =&gt; <span class="string">'Asia/Shanghai'</span>,</span><br><span class="line">];</span><br></pre></td></tr></tbody></table></figure><p>Trait 常用于复用一小段横向能力，但不要把它当成继承体系乱混。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">trait</span> <span class="title">HasTimestamps</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">now</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">        <span class="keyword">return</span> <span class="title function_ invoke__">date</span>(<span class="string">'Y-m-d H:i:s'</span>);</span><br><span class="line">    }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Article</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">use</span> <span class="title">HasTimestamps</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>内置开发服务器也从这个阶段开始方便本地临时调试：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">php -S 127.0.0.1:8000 -t public</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-5-5：Generator、finally、密码哈希"><a href="#PHP-5-5：Generator、finally、密码哈希" class="headerlink" title="PHP 5.5：Generator、finally、密码哈希"></a>PHP 5.5：Generator、finally、密码哈希</h3><p>Generator 适合处理大列表，避免一次性把所有数据塞进内存。</p><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">readLines</span>(<span class="params"><span class="variable">$file</span></span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="variable">$handle</span> = <span class="title function_ invoke__">fopen</span>(<span class="variable">$file</span>, <span class="string">'r'</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> {</span><br><span class="line">        <span class="keyword">while</span> ((<span class="variable">$line</span> = <span class="title function_ invoke__">fgets</span>(<span class="variable">$handle</span>)) !== <span class="literal">false</span>) {</span><br><span class="line">            <span class="keyword">yield</span> <span class="title function_ invoke__">trim</span>(<span class="variable">$line</span>);</span><br><span class="line">        }</span><br><span class="line">    } <span class="keyword">finally</span> {</span><br><span class="line">        <span class="title function_ invoke__">fclose</span>(<span class="variable">$handle</span>);</span><br><span class="line">    }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">foreach</span> (<span class="title function_ invoke__">readLines</span>(<span class="keyword">__DIR__</span> . <span class="string">'/access.log'</span>) <span class="keyword">as</span> <span class="variable">$line</span>) {</span><br><span class="line">    <span class="keyword">echo</span> <span class="variable">$line</span> . PHP_EOL;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>密码不应该再自己拼盐做 <code>md5()</code>。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$hash</span> = <span class="title function_ invoke__">password_hash</span>(<span class="string">'123456'</span>, PASSWORD_DEFAULT);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (<span class="title function_ invoke__">password_verify</span>(<span class="string">'123456'</span>, <span class="variable">$hash</span>)) {</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">'登录成功'</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-5-6：可变参数与参数解包"><a href="#PHP-5-6：可变参数与参数解包" class="headerlink" title="PHP 5.6：可变参数与参数解包"></a>PHP 5.6：可变参数与参数解包</h3><p>以前写不定参数通常要用 <code>func_get_args()</code>，PHP 5.6 开始可以直接写 <code>...</code>。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">sum</span>(<span class="params">...<span class="variable">$numbers</span></span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_ invoke__">array_sum</span>(<span class="variable">$numbers</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">sum</span>(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>);</span><br></pre></td></tr></tbody></table></figure><p>数组也可以解包到参数里。</p><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">makeUrl</span>(<span class="params"><span class="variable">$scheme</span>, <span class="variable">$host</span>, <span class="variable">$path</span></span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="keyword">return</span> <span class="variable">$scheme</span> . <span class="string">'://'</span> . <span class="variable">$host</span> . <span class="string">'/'</span> . <span class="title function_ invoke__">ltrim</span>(<span class="variable">$path</span>, <span class="string">'/'</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="variable">$args</span> = [<span class="string">'https'</span>, <span class="string">'example.com'</span>, <span class="string">'api/users'</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">makeUrl</span>(...<span class="variable">$args</span>);</span><br></pre></td></tr></tbody></table></figure><p>安全比较可以使用 <code>hash_equals()</code>，避免签名比较里的时序攻击问题。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$expected</span> = <span class="title function_ invoke__">hash_hmac</span>(<span class="string">'sha256'</span>, <span class="variable">$payload</span>, <span class="variable">$secret</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (!<span class="title function_ invoke__">hash_equals</span>(<span class="variable">$expected</span>, <span class="variable">$signature</span>)) {</span><br><span class="line">    <span class="keyword">exit</span>(<span class="string">'签名错误'</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="PHP-7：性能与类型系统升级"><a href="#PHP-7：性能与类型系统升级" class="headerlink" title="PHP 7：性能与类型系统升级"></a>PHP 7：性能与类型系统升级</h2><p>PHP 7 最大的感受是性能提升明显，语法也开始强调类型声明。</p><h3 id="PHP-7-0：标量类型、返回值、-、"><a href="#PHP-7-0：标量类型、返回值、-、" class="headerlink" title="PHP 7.0：标量类型、返回值、??、<=>"></a>PHP 7.0：标量类型、返回值、<code>??</code>、<code>&lt;=&gt;</code></h3><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">declare</span>(strict_types=<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">total</span>(<span class="params"><span class="keyword">int</span> <span class="variable">$price</span>, <span class="keyword">int</span> <span class="variable">$count</span></span>): <span class="title">int</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="keyword">return</span> <span class="variable">$price</span> * <span class="variable">$count</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">total</span>(<span class="number">20</span>, <span class="number">3</span>);</span><br></pre></td></tr></tbody></table></figure><p><code>??</code> 很适合处理请求参数和默认值。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$page</span> = <span class="variable">$_GET</span>[<span class="string">'page'</span>] ?? <span class="number">1</span>;</span><br><span class="line"><span class="variable">$keyword</span> = <span class="title function_ invoke__">trim</span>(<span class="variable">$_GET</span>[<span class="string">'keyword'</span>] ?? <span class="string">''</span>);</span><br></pre></td></tr></tbody></table></figure><p><code>&lt;=&gt;</code> 常用于排序。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="title function_ invoke__">usort</span>(<span class="variable">$users</span>, function (<span class="keyword">array</span> <span class="variable">$a</span>, <span class="keyword">array</span> <span class="variable">$b</span>) {</span><br><span class="line">    <span class="keyword">return</span> <span class="variable">$a</span>[<span class="string">'created_at'</span>] &lt;=&gt; <span class="variable">$b</span>[<span class="string">'created_at'</span>];</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure><p>PHP 7.0 也是很多老项目升级时最容易卡住的版本，因为 <code>mysql_*</code> 扩展已经移除。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 旧写法：PHP 7 不再支持</span></span><br><span class="line"><span class="comment">// mysql_query('select * from users');</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 新写法：改用 PDO 或 MySQLi</span></span><br><span class="line"><span class="variable">$stmt</span> = <span class="variable">$pdo</span>-&gt;<span class="title function_ invoke__">query</span>(<span class="string">'select id, name from users'</span>);</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-7-1：可空类型、void、多异常捕获"><a href="#PHP-7-1：可空类型、void、多异常捕获" class="headerlink" title="PHP 7.1：可空类型、void、多异常捕获"></a>PHP 7.1：可空类型、<code>void</code>、多异常捕获</h3><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">findUser</span>(<span class="params"><span class="keyword">int</span> <span class="variable">$id</span></span>): ?<span class="title">array</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="keyword">return</span> <span class="variable">$id</span> &gt; <span class="number">0</span> ? [<span class="string">'id'</span> =&gt; <span class="variable">$id</span>, <span class="string">'name'</span> =&gt; <span class="string">'Leroi'</span>] : <span class="literal">null</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">writeLog</span>(<span class="params"><span class="keyword">string</span> <span class="variable">$message</span></span>): <span class="title">void</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="title function_ invoke__">error_log</span>(<span class="variable">$message</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>多个异常可以合并捕获。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line">    <span class="variable">$service</span>-&gt;<span class="title function_ invoke__">handle</span>();</span><br><span class="line">} <span class="keyword">catch</span> (<span class="built_in">InvalidArgumentException</span> | <span class="built_in">RuntimeException</span> <span class="variable">$e</span>) {</span><br><span class="line">    <span class="title function_ invoke__">report</span>(<span class="variable">$e</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-7-2-到-7-3：object、Sodium、数组辅助函数"><a href="#PHP-7-2-到-7-3：object、Sodium、数组辅助函数" class="headerlink" title="PHP 7.2 到 7.3：object、Sodium、数组辅助函数"></a>PHP 7.2 到 7.3：<code>object</code>、Sodium、数组辅助函数</h3><p>PHP 7.2 增加 <code>object</code> 类型，适合约束必须传对象。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">touchModel</span>(<span class="params"><span class="keyword">object</span> <span class="variable">$model</span></span>): <span class="title">object</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="variable">$model</span>-&gt;updated_at = <span class="title function_ invoke__">date</span>(<span class="string">'Y-m-d H:i:s'</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="variable">$model</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>PHP 7.3 增加 <code>array_key_first()</code>、<code>array_key_last()</code> 和 <code>is_countable()</code>，处理数组边界更舒服。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (<span class="title function_ invoke__">is_countable</span>(<span class="variable">$items</span>) &amp;&amp; <span class="title function_ invoke__">count</span>(<span class="variable">$items</span>) &gt; <span class="number">0</span>) {</span><br><span class="line">    <span class="variable">$firstKey</span> = <span class="title function_ invoke__">array_key_first</span>(<span class="variable">$items</span>);</span><br><span class="line">    <span class="variable">$lastKey</span> = <span class="title function_ invoke__">array_key_last</span>(<span class="variable">$items</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-7-4：类型属性、箭头函数、"><a href="#PHP-7-4：类型属性、箭头函数、" class="headerlink" title="PHP 7.4：类型属性、箭头函数、??="></a>PHP 7.4：类型属性、箭头函数、<code>??=</code></h3><p>类型属性可以把 DTO 写得更清楚。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserData</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">int</span> <span class="variable">$id</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">string</span> <span class="variable">$name</span>;</span><br><span class="line">    <span class="keyword">public</span> ?<span class="keyword">string</span> <span class="variable">$mobile</span> = <span class="literal">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>箭头函数适合短回调。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$names</span> = <span class="title function_ invoke__">array_map</span>(</span><br><span class="line">    fn (<span class="keyword">array</span> <span class="variable">$user</span>) =&gt; <span class="variable">$user</span>[<span class="string">'name'</span>],</span><br><span class="line">    <span class="variable">$users</span></span><br><span class="line">);</span><br></pre></td></tr></tbody></table></figure><p><code>??=</code> 很适合补默认配置。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$config</span>[<span class="string">'timeout'</span>] ??= <span class="number">5</span>;</span><br><span class="line"><span class="variable">$config</span>[<span class="string">'retry'</span>] ??= <span class="number">3</span>;</span><br></pre></td></tr></tbody></table></figure><p>需要注意：类型属性未初始化就读取会报错。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Task</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">string</span> <span class="variable">$name</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="variable">$task</span> = <span class="keyword">new</span> <span class="title class_">Task</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// Fatal error: Typed property Task::$name must not be accessed before initialization</span></span><br><span class="line"><span class="comment">// echo $task-&gt;name;</span></span><br></pre></td></tr></tbody></table></figure><h2 id="PHP-8：语法现代化与强约束"><a href="#PHP-8：语法现代化与强约束" class="headerlink" title="PHP 8：语法现代化与强约束"></a>PHP 8：语法现代化与强约束</h2><p>PHP 8 更像现代语言：类型更强、对象写法更简洁、错误更明确。</p><h3 id="PHP-8-0：命名参数、Attribute、构造器属性提升"><a href="#PHP-8-0：命名参数、Attribute、构造器属性提升" class="headerlink" title="PHP 8.0：命名参数、Attribute、构造器属性提升"></a>PHP 8.0：命名参数、Attribute、构造器属性提升</h3><p>命名参数可以减少一长串可选参数的阅读成本。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">htmlspecialchars</span>(</span><br><span class="line">    <span class="attr">string</span>: <span class="variable">$content</span>,</span><br><span class="line">    <span class="attr">flags</span>: ENT_QUOTES,</span><br><span class="line">    <span class="attr">encoding</span>: <span class="string">'UTF-8'</span>,</span><br><span class="line">    <span class="attr">double_encode</span>: <span class="literal">false</span></span><br><span class="line">);</span><br></pre></td></tr></tbody></table></figure><p>Attribute 可以替代一部分注解写法。</p><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserController</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="meta">#[Route</span>(<span class="string">'/api/users/{id}'</span>, <span class="attr">methods</span>: [<span class="string">'GET'</span>])<span class="meta">]</span></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">show</span>(<span class="params"><span class="keyword">int</span> <span class="variable">$id</span></span>): <span class="title">array</span></span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">$this</span>-&gt;userService-&gt;<span class="title function_ invoke__">find</span>(<span class="variable">$id</span>);</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>构造器属性提升让值对象更短。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Point</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">float</span> <span class="variable">$x</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">float</span> <span class="variable">$y</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">float</span> <span class="variable">$z</span> = <span class="number">0.0</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">    </span>) </span>{</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p><code>match</code> 比 <code>switch</code> 更严格，也会返回值。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$label</span> = <span class="keyword">match</span> (<span class="variable">$status</span>) {</span><br><span class="line">    <span class="number">0</span> =&gt; <span class="string">'待支付'</span>,</span><br><span class="line">    <span class="number">1</span> =&gt; <span class="string">'已支付'</span>,</span><br><span class="line">    <span class="number">2</span> =&gt; <span class="string">'已取消'</span>,</span><br><span class="line">    <span class="keyword">default</span> =&gt; <span class="string">'未知状态'</span>,</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure><p>空安全操作符适合处理可能为空的对象链。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$city</span> = <span class="variable">$order</span>-&gt;user?-&gt;profile?-&gt;city ?? <span class="string">'未知'</span>;</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-8-1：Enum-与-readonly"><a href="#PHP-8-1：Enum-与-readonly" class="headerlink" title="PHP 8.1：Enum 与 readonly"></a>PHP 8.1：Enum 与 readonly</h3><p>状态常量可以逐步迁移成 Enum。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">OrderStatus</span>: <span class="title">string</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">case</span> Pending = <span class="string">'pending'</span>;</span><br><span class="line">    <span class="keyword">case</span> Paid = <span class="string">'paid'</span>;</span><br><span class="line">    <span class="keyword">case</span> Closed = <span class="string">'closed'</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">canRefund</span>(<span class="params">OrderStatus <span class="variable">$status</span></span>): <span class="title">bool</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="keyword">return</span> <span class="variable">$status</span> === <span class="title class_">OrderStatus</span>::<span class="variable constant_">Paid</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>只读属性适合值对象、配置对象和查询结果对象。</p><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Config</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">readonly</span> <span class="keyword">string</span> <span class="variable">$appName</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">readonly</span> <span class="keyword">string</span> <span class="variable">$env</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">    </span>) </span>{</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-8-2：只读类与动态属性弃用"><a href="#PHP-8-2：只读类与动态属性弃用" class="headerlink" title="PHP 8.2：只读类与动态属性弃用"></a>PHP 8.2：只读类与动态属性弃用</h3><p>只读类可以减少重复写 <code>readonly</code>。</p><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">readonly</span> <span class="class"><span class="keyword">class</span> <span class="title">UserSnapshot</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">int</span> <span class="variable">$id</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">string</span> <span class="variable">$name</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">    </span>) </span>{</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>动态属性弃用是老项目升级 PHP 8.2 时常见问题。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">User</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">string</span> <span class="variable">$name</span> = <span class="string">''</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="variable">$user</span> = <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// Deprecated: Creation of dynamic property User::$age is deprecated</span></span><br><span class="line"><span class="variable">$user</span>-&gt;age = <span class="number">18</span>;</span><br></pre></td></tr></tbody></table></figure><p>应改成显式声明属性，或者用数组保存动态字段。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">User</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">string</span> <span class="variable">$name</span> = <span class="string">''</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">int</span> <span class="variable">$age</span> = <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-8-3：类型化类常量、-Override-、JSON-校验"><a href="#PHP-8-3：类型化类常量、-Override-、JSON-校验" class="headerlink" title="PHP 8.3：类型化类常量、#[Override]、JSON 校验"></a>PHP 8.3：类型化类常量、<code>#[Override]</code>、JSON 校验</h3><p>类型化类常量能避免子类或实现类把常量改成奇怪类型。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">ApiVersion</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">const</span> <span class="variable constant_">string</span> VERSION = <span class="string">'v1'</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p><code>#[Override]</code> 可以帮你发现方法名写错。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">BaseController</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">boot</span>(<span class="params"></span>): <span class="title">void</span></span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">    }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserController</span> <span class="keyword">extends</span> <span class="title">BaseController</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="meta">#[Override</span><span class="meta">]</span></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">boot</span>(<span class="params"></span>): <span class="title">void</span></span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">        <span class="built_in">parent</span>::<span class="title function_ invoke__">boot</span>();</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p><code>json_validate()</code> 适合只判断 JSON 是否合法，不需要马上解码。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (!<span class="title function_ invoke__">json_validate</span>(<span class="variable">$rawBody</span>)) {</span><br><span class="line">    <span class="title function_ invoke__">http_response_code</span>(<span class="number">400</span>);</span><br><span class="line">    <span class="keyword">exit</span>(<span class="string">'JSON 格式错误'</span>);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-8-4：属性钩子与非对称可见性"><a href="#PHP-8-4：属性钩子与非对称可见性" class="headerlink" title="PHP 8.4：属性钩子与非对称可见性"></a>PHP 8.4：属性钩子与非对称可见性</h3><p>属性钩子可以把简单 getter/setter 合并到属性声明里。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserName</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">string</span> <span class="variable">$name</span> {</span><br><span class="line">        set =&gt; <span class="title function_ invoke__">trim</span>(<span class="variable">$value</span>);</span><br><span class="line">    }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="variable">$user</span> = <span class="keyword">new</span> <span class="title class_">UserName</span>();</span><br><span class="line"><span class="variable">$user</span>-&gt;name = <span class="string">' Leroi '</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="variable">$user</span>-&gt;name;</span><br></pre></td></tr></tbody></table></figure><p>非对称可见性适合“外部可读，内部或构造阶段可写”的模型。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Article</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">private</span>(set) <span class="keyword">string</span> <span class="variable">$title</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"><span class="keyword">string</span> <span class="variable">$title</span></span>)</span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">        <span class="variable language_">$this</span>-&gt;title = <span class="variable">$title</span>;</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p><code>new</code> 后直接链式调用也更自然。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$date</span> = <span class="keyword">new</span> <span class="title class_">DateTimeImmutable</span>(<span class="string">'now'</span>)-&gt;<span class="title function_ invoke__">format</span>(<span class="string">'Y-m-d'</span>);</span><br></pre></td></tr></tbody></table></figure><h3 id="PHP-8-5：URI、管道操作符与-clone-with"><a href="#PHP-8-5：URI、管道操作符与-clone-with" class="headerlink" title="PHP 8.5：URI、管道操作符与 clone with"></a>PHP 8.5：URI、管道操作符与 clone with</h3><p>PHP 8.5 新增 URI 扩展，适合更标准地处理 URL。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Uri</span>\<span class="title">Rfc3986</span>\<span class="title">Uri</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable">$uri</span> = <span class="keyword">new</span> <span class="title class_">Uri</span>(<span class="string">'https://example.com/docs/php?page=1'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="variable">$uri</span>-&gt;<span class="title function_ invoke__">getHost</span>();</span><br></pre></td></tr></tbody></table></figure><p>管道操作符让一串转换按从上到下的顺序阅读。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$slug</span> = <span class="string">' PHP 8.5 Released '</span></span><br><span class="line">    |&gt; <span class="title function_ invoke__">trim</span>(...)</span><br><span class="line">    |&gt; (<span class="function"><span class="keyword">fn</span> (<span class="params"><span class="keyword">string</span> <span class="variable">$value</span></span>) =&gt;</span> <span class="title function_ invoke__">str_replace</span>(<span class="string">' '</span>, <span class="string">'-'</span>, <span class="variable">$value</span>))</span><br><span class="line">    |&gt; <span class="title function_ invoke__">strtolower</span>(...);</span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="variable">$slug</span>;</span><br></pre></td></tr></tbody></table></figure><p><code>clone()</code> 支持克隆时修改属性，对只读对象的“改一个字段生成新对象”很有用。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">readonly</span> <span class="class"><span class="keyword">class</span> <span class="title">Color</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">int</span> <span class="variable">$red</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">int</span> <span class="variable">$green</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">int</span> <span class="variable">$blue</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">        <span class="keyword">public</span> <span class="keyword">int</span> <span class="variable">$alpha</span> = <span class="number">255</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">    </span>) </span>{</span><br><span class="line">    }</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">withAlpha</span>(<span class="params"><span class="keyword">int</span> <span class="variable">$alpha</span></span>): <span class="title">self</span></span></span><br><span class="line"><span class="function">    </span>{</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">clone</span>(<span class="variable language_">$this</span>, [</span><br><span class="line">            <span class="string">'alpha'</span> =&gt; <span class="variable">$alpha</span>,</span><br><span class="line">        ]);</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p><code>#[NoDiscard]</code> 可以提醒调用方不要忽略重要返回值。</p><figure class="highlight php"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#[\NoDiscard</span><span class="meta">]</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createToken</span>(<span class="params"><span class="keyword">int</span> <span class="variable">$userId</span></span>): <span class="title">string</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_ invoke__">bin2hex</span>(<span class="title function_ invoke__">random_bytes</span>(<span class="number">16</span>));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="title function_ invoke__">createToken</span>(<span class="number">1</span>);</span><br></pre></td></tr></tbody></table></figure><h2 id="老项目升级路线"><a href="#老项目升级路线" class="headerlink" title="老项目升级路线"></a>老项目升级路线</h2><p>不要从 PHP 5.3、5.4 直接盲跳到 PHP 8.5。更稳的做法是先清点依赖，再分阶段处理兼容问题。</p><h2 id="1-先确认当前运行环境"><a href="#1-先确认当前运行环境" class="headerlink" title="1. 先确认当前运行环境"></a>1. 先确认当前运行环境</h2><figure class="highlight bash"><table><tbody><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">php -v</span><br><span class="line">php -m</span><br><span class="line">php --ini</span><br><span class="line">composer --version</span><br></pre></td></tr></tbody></table></figure><h2 id="2-固定-Composer-平台版本"><a href="#2-固定-Composer-平台版本" class="headerlink" title="2. 固定 Composer 平台版本"></a>2. 固定 Composer 平台版本</h2><p>本地开发和服务器版本不一致时，先在 <code>composer.json</code> 里明确平台版本，避免装到线上跑不了的包。</p><figure class="highlight json"><table><tbody><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="punctuation">{</span></span><br><span class="line">  <span class="attr">"config"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line">    <span class="attr">"platform"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line">      <span class="attr">"php"</span><span class="punctuation">:</span> <span class="string">"8.2.0"</span></span><br><span class="line">    <span class="punctuation">}</span></span><br><span class="line">  <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><p>检查依赖为什么不能升级：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">composer why-not php 8.2</span><br><span class="line">composer outdated</span><br></pre></td></tr></tbody></table></figure><h2 id="3-优先处理高风险旧写法"><a href="#3-优先处理高风险旧写法" class="headerlink" title="3. 优先处理高风险旧写法"></a>3. 优先处理高风险旧写法</h2><div class="md-table-scroll"><table><thead><tr><th>旧写法</th><th>问题</th><th>建议</th></tr></thead><tbody><tr><td><code>mysql_query()</code></td><td>PHP 7 已移除</td><td>改 PDO 或 MySQLi</td></tr><tr><td>动态属性</td><td>PHP 8.2 起弃用</td><td>显式声明属性或使用数组</td></tr><tr><td>魔术引号相关逻辑</td><td>PHP 5.4 已移除</td><td>删除兼容代码，统一输入过滤</td></tr><tr><td>老式构造函数</td><td>PHP 7/8 迁移容易混乱</td><td>统一改 <code>__construct()</code></td></tr><tr><td>依赖 warning 的流程</td><td>PHP 8 很多错误更严格</td><td>用异常和显式判断改写</td></tr><tr><td>未初始化类型属性</td><td>PHP 7.4 起会报错</td><td>给默认值或在构造函数初始化</td></tr></tbody></table></div><h2 id="4-一段旧代码的升级示例"><a href="#4-一段旧代码的升级示例" class="headerlink" title="4. 一段旧代码的升级示例"></a>4. 一段旧代码的升级示例</h2><p>老写法：</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$id</span> = <span class="title function_ invoke__">intval</span>(<span class="variable">$_GET</span>[<span class="string">'id'</span>]);</span><br><span class="line"><span class="variable">$sql</span> = <span class="string">"select * from users where id = <span class="subst">{$id}</span>"</span>;</span><br><span class="line"><span class="variable">$result</span> = <span class="title function_ invoke__">mysql_query</span>(<span class="variable">$sql</span>);</span><br><span class="line"><span class="variable">$user</span> = <span class="title function_ invoke__">mysql_fetch_assoc</span>(<span class="variable">$result</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">json_encode</span>(<span class="variable">$user</span>);</span><br></pre></td></tr></tbody></table></figure><p>较新的写法：</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$id</span> = (<span class="keyword">int</span>) (<span class="variable">$_GET</span>[<span class="string">'id'</span>] ?? <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable">$stmt</span> = <span class="variable">$pdo</span>-&gt;<span class="title function_ invoke__">prepare</span>(<span class="string">'select id, name, mobile from users where id = ? limit 1'</span>);</span><br><span class="line"><span class="variable">$stmt</span>-&gt;<span class="title function_ invoke__">execute</span>([<span class="variable">$id</span>]);</span><br><span class="line"></span><br><span class="line"><span class="variable">$user</span> = <span class="variable">$stmt</span>-&gt;<span class="title function_ invoke__">fetch</span>(PDO::<span class="variable constant_">FETCH_ASSOC</span>);</span><br><span class="line"></span><br><span class="line"><span class="title function_ invoke__">header</span>(<span class="string">'Content-Type: application/json; charset=utf-8'</span>);</span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">json_encode</span>(<span class="variable">$user</span> ?: [], JSON_UNESCAPED_UNICODE);</span><br></pre></td></tr></tbody></table></figure><p>再进一步，可以把输入、查询和输出拆开，后续迁移框架也更容易。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">findUser</span>(<span class="params">PDO <span class="variable">$pdo</span>, <span class="keyword">int</span> <span class="variable">$id</span></span>): ?<span class="title">array</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line">    <span class="variable">$stmt</span> = <span class="variable">$pdo</span>-&gt;<span class="title function_ invoke__">prepare</span>(<span class="string">'select id, name, mobile from users where id = ? limit 1'</span>);</span><br><span class="line">    <span class="variable">$stmt</span>-&gt;<span class="title function_ invoke__">execute</span>([<span class="variable">$id</span>]);</span><br><span class="line"></span><br><span class="line">    <span class="variable">$user</span> = <span class="variable">$stmt</span>-&gt;<span class="title function_ invoke__">fetch</span>(PDO::<span class="variable constant_">FETCH_ASSOC</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="variable">$user</span> === <span class="literal">false</span> ? <span class="literal">null</span> : <span class="variable">$user</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="variable">$id</span> = <span class="title function_ invoke__">filter_input</span>(INPUT_GET, <span class="string">'id'</span>, FILTER_VALIDATE_INT);</span><br><span class="line"><span class="variable">$user</span> = <span class="variable">$id</span> ? <span class="title function_ invoke__">findUser</span>(<span class="variable">$pdo</span>, <span class="variable">$id</span>) : <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"><span class="title function_ invoke__">header</span>(<span class="string">'Content-Type: application/json; charset=utf-8'</span>);</span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">json_encode</span>(<span class="variable">$user</span> ?? [], JSON_UNESCAPED_UNICODE);</span><br></pre></td></tr></tbody></table></figure><h2 id="新项目怎么选版本"><a href="#新项目怎么选版本" class="headerlink" title="新项目怎么选版本"></a>新项目怎么选版本</h2><div class="md-table-scroll"><table><thead><tr><th>场景</th><th>建议</th></tr></thead><tbody><tr><td>全新项目</td><td>优先选官方仍在支持的较新版本，并确认框架、扩展、服务器镜像都支持</td></tr><tr><td>ThinkPHP 8 项目</td><td>优先确认 ThinkPHP、Composer 包、PHP 扩展和部署面板支持的 PHP 版本</td></tr><tr><td>老项目维护</td><td>先升级到当前依赖能承受的版本，再逐步替换旧扩展和旧语法</td></tr><tr><td>商业项目</td><td>不建议运行 EOL 版本，至少要保证安全支持期内</td></tr><tr><td>面板环境</td><td>先看宝塔、1Panel、MAMP、XAMPP 提供的 PHP 版本和扩展完整度</td></tr></tbody></table></div><p>实际项目里，版本选择不是“越新越好”这么简单。新版本语法好用，但线上服务器、Composer 包、扩展、框架版本、部署面板都要一起确认。</p><h2 id="常见升级报错"><a href="#常见升级报错" class="headerlink" title="常见升级报错"></a>常见升级报错</h2><h3 id="Call-to-undefined-function-mysql-connect"><a href="#Call-to-undefined-function-mysql-connect" class="headerlink" title="Call to undefined function mysql_connect()"></a>Call to undefined function mysql_connect()</h3><p>PHP 7 已移除 <code>mysql_*</code> 扩展。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Fatal error: Uncaught Error: Call to undefined function mysql_connect()</span><br></pre></td></tr></tbody></table></figure><p>处理方法：改成 PDO 或 MySQLi，不建议继续寻找旧扩展补丁。</p><h3 id="Creation-of-dynamic-property-is-deprecated"><a href="#Creation-of-dynamic-property-is-deprecated" class="headerlink" title="Creation of dynamic property is deprecated"></a>Creation of dynamic property is deprecated</h3><p>PHP 8.2 开始不推荐动态属性。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Deprecated: Creation of dynamic property User::$age is deprecated</span><br></pre></td></tr></tbody></table></figure><p>处理方法：显式声明属性。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">User</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">int</span> <span class="variable">$age</span> = <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="Typed-property-must-not-be-accessed-before-initialization"><a href="#Typed-property-must-not-be-accessed-before-initialization" class="headerlink" title="Typed property must not be accessed before initialization"></a>Typed property must not be accessed before initialization</h3><p>PHP 7.4 类型属性未初始化就读取会报错。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Fatal error: Typed property User::$name must not be accessed before initialization</span><br></pre></td></tr></tbody></table></figure><p>处理方法：给默认值，或者在构造函数中初始化。</p><figure class="highlight php"><table><tbody><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="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">User</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">string</span> <span class="variable">$name</span> = <span class="string">''</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="Composer-PHP-version-does-not-satisfy"><a href="#Composer-PHP-version-does-not-satisfy" class="headerlink" title="Composer PHP version does not satisfy"></a>Composer PHP version does not satisfy</h3><p>本机 PHP 版本和依赖要求不匹配。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Your PHP version does not satisfy that requirement.</span><br></pre></td></tr></tbody></table></figure><p>处理方法：先看当前 PHP 版本和依赖约束。</p><figure class="highlight bash"><table><tbody><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">php -v</span><br><span class="line">composer why-not php 8.2</span><br><span class="line">composer show package/name --all</span><br></pre></td></tr></tbody></table></figure><h2 id="官方入口"><a href="#官方入口" class="headerlink" title="官方入口"></a>官方入口</h2><ul><li><a target="_blank" rel="noopener" href="https://www.php.net/supported-versions.php">PHP 支持版本</a></li><li><a target="_blank" rel="noopener" href="https://www.php.net/releases/8.5/en.php">PHP 8.5 发布说明</a></li><li><a target="_blank" rel="noopener" href="https://www.php.net/releases/8.4/en.php">PHP 8.4 发布说明</a></li><li><a target="_blank" rel="noopener" href="https://www.php.net/manual/en/migration80.php">PHP 8 迁移指南</a></li><li><a target="_blank" rel="noopener" href="https://www.php.net/manual/en/migration70.php">PHP 7 迁移指南</a></li><li><a target="_blank" rel="noopener" href="https://www.php.net/manual/php5.php">PHP 5 旧文档说明</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/php-php-version-history/</id>
    <link href="https://blogs.microcyan.com/posts/php-php-version-history/"/>
    <published>2026-06-01T14:51:00.000Z</published>
    <summary>从 PHP 5.0 到 PHP 8.5 的版本升级变化、语法示例、兼容注意事项和老项目升级路线。</summary>
    <title>PHP 5 到 PHP 8.5 版本升级变化</title>
    <updated>2026-09-08T10:34:43.299Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="物联网与机器人" scheme="https://blogs.microcyan.com/categories/iot-robotics/"/>
    <category term="物联网" scheme="https://blogs.microcyan.com/tags/%E7%89%A9%E8%81%94%E7%BD%91/"/>
    <category term="嵌入式开发" scheme="https://blogs.microcyan.com/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F%E5%BC%80%E5%8F%91/"/>
    <category term="SLAM" scheme="https://blogs.microcyan.com/tags/SLAM/"/>
    <content>
      <![CDATA[<!-- more --><p>SLAM 是 Simultaneous Localization and Mapping，同步定位与建图。它解决的是：机器人或设备在未知环境里，一边估计自己在哪里，一边构建周围环境地图。</p><p>SLAM 常见于机器人、无人车、无人机、AR、扫地机、移动测绘、仓储 AGV 和自动驾驶感知定位。</p><h2 id="一句话理解"><a href="#一句话理解" class="headerlink" title="一句话理解"></a>一句话理解</h2><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">传感器数据 -&gt; 估计当前位姿 -&gt; 构建地图 -&gt; 修正累计误差 -&gt; 输出可用定位和地图</span><br></pre></td></tr></tbody></table></figure><p>其中最关键的问题是误差会累积。只靠里程计或相机连续估计，走久了就会漂移，所以 SLAM 需要后端优化和回环检测来把轨迹拉回正确位置。</p><h2 id="基本术语"><a href="#基本术语" class="headerlink" title="基本术语"></a>基本术语</h2><div class="md-table-scroll"><table><thead><tr><th>术语</th><th>含义</th></tr></thead><tbody><tr><td>Pose</td><td>位姿，通常包含位置和姿态</td></tr><tr><td>Odometry</td><td>里程计，估计相邻时刻运动</td></tr><tr><td>Landmark</td><td>路标点，环境中可重复观测的特征</td></tr><tr><td>Front-end</td><td>前端，负责特征提取、匹配、跟踪、里程计</td></tr><tr><td>Back-end</td><td>后端，负责优化轨迹和地图</td></tr><tr><td>Loop Closure</td><td>回环检测，识别回到旧位置</td></tr><tr><td>Map</td><td>地图，可以是稀疏点云、稠密点云、栅格图、语义地图</td></tr><tr><td>IMU</td><td>惯性测量单元，提供加速度和角速度</td></tr></tbody></table></div><h2 id="SLAM-系统常见结构"><a href="#SLAM-系统常见结构" class="headerlink" title="SLAM 系统常见结构"></a>SLAM 系统常见结构</h2><figure class="highlight text"><table><tbody><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><br><span class="line">  -&gt; 数据预处理</span><br><span class="line">  -&gt; 前端里程计</span><br><span class="line">  -&gt; 局部地图</span><br><span class="line">  -&gt; 后端优化</span><br><span class="line">  -&gt; 回环检测</span><br><span class="line">  -&gt; 全局地图</span><br></pre></td></tr></tbody></table></figure><p>前端追求实时，后端追求全局一致性。</p><h2 id="按传感器分类"><a href="#按传感器分类" class="headerlink" title="按传感器分类"></a>按传感器分类</h2><div class="md-table-scroll"><table><thead><tr><th>类型</th><th>传感器</th><th>特点</th></tr></thead><tbody><tr><td>视觉 SLAM</td><td>单目、双目、RGB-D 相机</td><td>成本低，受光照和纹理影响大</td></tr><tr><td>激光 SLAM</td><td>2D/3D LiDAR</td><td>几何精度高，成本和数据量较高</td></tr><tr><td>视觉惯性 SLAM</td><td>相机 + IMU</td><td>运动估计更稳，标定要求更高</td></tr><tr><td>激光惯性 SLAM</td><td>LiDAR + IMU</td><td>适合移动机器人和室外场景</td></tr><tr><td>多传感器 SLAM</td><td>相机、LiDAR、IMU、轮速计、GPS</td><td>鲁棒性更好，系统复杂度更高</td></tr></tbody></table></div><h2 id="按算法思想分类"><a href="#按算法思想分类" class="headerlink" title="按算法思想分类"></a>按算法思想分类</h2><h3 id="滤波-SLAM"><a href="#滤波-SLAM" class="headerlink" title="滤波 SLAM"></a>滤波 SLAM</h3><p>早期常见方法，包括 EKF-SLAM、FastSLAM 等。</p><p>特点：</p><ul><li>以概率滤波方式递推状态。</li><li>适合理解 SLAM 基础。</li><li>在大规模环境里状态维度和计算压力会变大。</li></ul><h3 id="图优化-SLAM"><a href="#图优化-SLAM" class="headerlink" title="图优化 SLAM"></a>图优化 SLAM</h3><p>现代 SLAM 常用思路，把位姿和约束表示成图，再做非线性优化。</p><figure class="highlight text"><table><tbody><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><br><span class="line">边：里程计约束、观测约束、回环约束、IMU 约束</span><br><span class="line">目标：让所有约束误差整体最小</span><br></pre></td></tr></tbody></table></figure><p>优点：</p><ul><li>更适合大规模场景。</li><li>能自然加入回环。</li><li>可以融合多传感器约束。</li></ul><h2 id="视觉-SLAM"><a href="#视觉-SLAM" class="headerlink" title="视觉 SLAM"></a>视觉 SLAM</h2><p>视觉 SLAM 主要依赖图像。</p><p>常见路线：</p><div class="md-table-scroll"><table><thead><tr><th>路线</th><th>思路</th></tr></thead><tbody><tr><td>特征点法</td><td>提取 ORB、SIFT、SURF 等特征，再匹配跟踪</td></tr><tr><td>直接法</td><td>直接利用像素灰度误差估计运动</td></tr><tr><td>半直接法</td><td>结合特征和直接法思想</td></tr></tbody></table></div><p>典型问题：</p><ul><li>光照变化导致匹配失败。</li><li>白墙、玻璃、纯色地面缺少纹理。</li><li>快速运动造成模糊。</li><li>单目相机尺度不确定。</li><li>动态物体干扰特征匹配。</li></ul><p>常见处理：</p><ul><li>增加 IMU。</li><li>使用双目或 RGB-D。</li><li>提高快门速度和图像质量。</li><li>剔除动态区域。</li><li>做回环检测和重定位。</li></ul><h2 id="激光-SLAM"><a href="#激光-SLAM" class="headerlink" title="激光 SLAM"></a>激光 SLAM</h2><p>激光 SLAM 依赖 LiDAR 扫描。</p><p>常见路线：</p><ul><li>2D 激光 + 栅格地图。</li><li>3D LiDAR + 点云匹配。</li><li>LiDAR + IMU 融合。</li></ul><p>常见算法模块：</p><div class="md-table-scroll"><table><thead><tr><th>模块</th><th>作用</th></tr></thead><tbody><tr><td>Scan Matching</td><td>当前扫描和地图匹配</td></tr><tr><td>ICP</td><td>点云配准</td></tr><tr><td>NDT</td><td>点云概率栅格匹配</td></tr><tr><td>Deskew</td><td>消除运动畸变</td></tr><tr><td>Loop Closure</td><td>闭环修正</td></tr></tbody></table></div><p>常见问题：</p><ul><li>长走廊退化，几何约束不足。</li><li>玻璃、雨雾、反光物体导致异常点。</li><li>运动太快，点云畸变严重。</li><li>IMU 外参或时间同步不准。</li></ul><h2 id="VIO-视觉惯性里程计"><a href="#VIO-视觉惯性里程计" class="headerlink" title="VIO 视觉惯性里程计"></a>VIO 视觉惯性里程计</h2><p>VIO 使用相机和 IMU 融合估计运动。</p><p>优势：</p><ul><li>比纯视觉更抗快速运动。</li><li>IMU 可以补充短时间图像跟踪失败。</li><li>适合无人机、AR、手持设备。</li></ul><p>难点：</p><ul><li>相机和 IMU 时间同步。</li><li>外参标定。</li><li>IMU 噪声建模。</li><li>初始化尺度、重力方向和偏置。</li></ul><h2 id="回环检测为什么重要"><a href="#回环检测为什么重要" class="headerlink" title="回环检测为什么重要"></a>回环检测为什么重要</h2><p>只靠前端里程计，误差会一直积累。回环检测用于识别“我又回到了曾经来过的地方”。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">发现回环 -&gt; 添加回环约束 -&gt; 后端优化 -&gt; 修正整条轨迹</span><br></pre></td></tr></tbody></table></figure><p>没有回环时，地图可能越来越歪；有错误回环时，地图可能被拉坏。所以回环检测既要召回，也要防误匹配。</p><h2 id="地图表示"><a href="#地图表示" class="headerlink" title="地图表示"></a>地图表示</h2><div class="md-table-scroll"><table><thead><tr><th>地图</th><th>用途</th></tr></thead><tbody><tr><td>稀疏点云</td><td>定位、回环、视觉地图</td></tr><tr><td>稠密点云</td><td>重建、测绘、三维展示</td></tr><tr><td>栅格地图</td><td>2D 导航、避障</td></tr><tr><td>OctoMap</td><td>3D 占据地图</td></tr><tr><td>TSDF/ESDF</td><td>三维重建和路径规划</td></tr><tr><td>语义地图</td><td>带物体类别和场景理解</td></tr></tbody></table></div><p>不是地图越密越好。导航常用栅格地图，视觉定位可能只需要稀疏地图。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="SLAM-和导航是什么关系"><a href="#SLAM-和导航是什么关系" class="headerlink" title="SLAM 和导航是什么关系"></a>SLAM 和导航是什么关系</h3><p>SLAM 负责定位和建图，导航负责根据地图规划路径并控制机器人移动。导航依赖定位，但 SLAM 不等于完整导航系统。</p><h3 id="单目-SLAM-为什么有尺度问题"><a href="#单目-SLAM-为什么有尺度问题" class="headerlink" title="单目 SLAM 为什么有尺度问题"></a>单目 SLAM 为什么有尺度问题</h3><p>单目相机只看到二维图像，无法直接知道真实距离。没有额外信息时，只能得到相对尺度。可以通过双目、RGB-D、IMU、已知物体尺寸或高度约束补尺度。</p><h3 id="为什么机器人走久了地图会歪"><a href="#为什么机器人走久了地图会歪" class="headerlink" title="为什么机器人走久了地图会歪"></a>为什么机器人走久了地图会歪</h3><p>前端里程计有误差，每一帧叠加一点，时间长了就会漂移。需要后端优化、回环检测、传感器融合来修正。</p><h3 id="为什么特征点突然丢失"><a href="#为什么特征点突然丢失" class="headerlink" title="为什么特征点突然丢失"></a>为什么特征点突然丢失</h3><p>常见原因：</p><ul><li>画面模糊。</li><li>光照突变。</li><li>纹理太少。</li><li>物体快速移动。</li><li>相机曝光不稳定。</li></ul><p>处理：</p><ul><li>降低运动速度。</li><li>提高图像质量。</li><li>使用更合适的相机。</li><li>融合 IMU。</li></ul><h3 id="激光-SLAM-在长走廊里为什么不稳"><a href="#激光-SLAM-在长走廊里为什么不稳" class="headerlink" title="激光 SLAM 在长走廊里为什么不稳"></a>激光 SLAM 在长走廊里为什么不稳</h3><p>长走廊几何结构重复，横向约束不足，匹配容易退化。可以融合 IMU、轮速计、视觉，或增加人工标志物、优化环境特征。</p><h3 id="SLAM-一定要-ROS-吗"><a href="#SLAM-一定要-ROS-吗" class="headerlink" title="SLAM 一定要 ROS 吗"></a>SLAM 一定要 ROS 吗</h3><p>不一定。ROS 方便传感器接入、消息通信、可视化和算法集成，但 SLAM 算法本身可以脱离 ROS 运行。学习和机器人项目中，ROS 2 会让工程组织更方便。</p><h2 id="学习路线"><a href="#学习路线" class="headerlink" title="学习路线"></a>学习路线</h2><ol><li>先理解坐标系、旋转、平移、矩阵、四元数。</li><li>学 OpenCV 基础，理解相机模型和特征匹配。</li><li>学非线性优化，理解图优化思想。</li><li>跑通一个开源视觉 SLAM 或激光 SLAM 示例。</li><li>学 ROS 2、TF、rosbag、RViz。</li><li>再进入 VIO、LiDAR-Inertial、多传感器融合。</li></ol><h2 id="工程排查清单"><a href="#工程排查清单" class="headerlink" title="工程排查清单"></a>工程排查清单</h2><ul><li>传感器时间戳是否准确。</li><li>相机内参是否标定。</li><li>相机和 IMU、LiDAR 外参是否正确。</li><li>坐标系方向是否统一。</li><li>rosbag 数据是否丢帧。</li><li>运动速度是否超过算法承受范围。</li><li>环境纹理或几何特征是否足够。</li><li>回环是否误匹配。</li><li>CPU/GPU 性能是否足够实时运行。</li></ul><h2 id="常见开源方向"><a href="#常见开源方向" class="headerlink" title="常见开源方向"></a>常见开源方向</h2><ul><li>ORB-SLAM 系列：经典视觉 SLAM。</li><li>VINS 系列：视觉惯性方向。</li><li>Cartographer：2D/3D 激光 SLAM。</li><li>LOAM 系列：激光里程计和建图。</li><li>LIO-SAM：LiDAR、IMU、因子图融合方向。</li></ul><p>学习时不要一开始就追求“最强算法”，先跑通数据、理解坐标系、看懂前端和后端各自做什么。</p><h2 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h2><ul><li>ORB-SLAM 论文：<a target="_blank" rel="noopener" href="https://arxiv.org/abs/1502.00956">https://arxiv.org/abs/1502.00956</a></li><li>ORB-SLAM2 论文：<a target="_blank" rel="noopener" href="https://arxiv.org/abs/1610.06475">https://arxiv.org/abs/1610.06475</a></li><li>ORB-SLAM3 论文：<a target="_blank" rel="noopener" href="https://arxiv.org/abs/2007.11898">https://arxiv.org/abs/2007.11898</a></li><li>ROS 2 快速入门：/iot/ros2-quickstart</li><li>OpenCV 快速入门：/vision/opencv</li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/iot-slam-algorithms/</id>
    <link href="https://blogs.microcyan.com/posts/iot-slam-algorithms/"/>
    <published>2026-05-27T15:34:00.000Z</published>
    <summary>整理 SLAM 同步定位与建图的核心概念、算法分类、视觉 SLAM、激光 SLAM、VIO、后端优化、回环检测、地图表示和常见问题。</summary>
    <title>SLAM 算法快速入门与常见问题</title>
    <updated>2026-09-08T10:34:43.299Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="物联网与机器人" scheme="https://blogs.microcyan.com/categories/iot-robotics/"/>
    <category term="物联网" scheme="https://blogs.microcyan.com/tags/%E7%89%A9%E8%81%94%E7%BD%91/"/>
    <category term="嵌入式开发" scheme="https://blogs.microcyan.com/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F%E5%BC%80%E5%8F%91/"/>
    <category term="ROS 2" scheme="https://blogs.microcyan.com/tags/ROS-2/"/>
    <content>
      <![CDATA[<!-- more --><p>ROS 2 是机器人软件开发里常见的基础框架，用来组织节点、话题、服务、动作、参数、坐标变换和数据记录。它不是一个单独的库，而是一套让机器人模块协作的通信和工程体系。</p><p>如果只是做普通物联网设备，MQTT、HTTP、串口协议可能已经足够；如果项目里有传感器融合、运动控制、地图、导航、机械臂、仿真、多个进程协同，ROS 2 会更合适。</p><blockquote><p><strong>官方入口</strong></p><ul><li><a target="_blank" rel="noopener" href="https://docs.ros.org/">ROS 2 官方文档</a></li><li><a target="_blank" rel="noopener" href="https://docs.ros.org/en/lyrical/Installation/Ubuntu-Install-Debs.html">ROS 2 Lyrical 安装</a></li><li><a target="_blank" rel="noopener" href="https://docs.ros.org/en/jazzy/Installation/Ubuntu-Install-Debs.html">ROS 2 Jazzy 安装</a></li><li><a target="_blank" rel="noopener" href="https://docs.ros.org/en/jazzy/Tutorials.html">ROS 2 Beginner Tutorials</a></li></ul></blockquote><h2 id="版本怎么选"><a href="#版本怎么选" class="headerlink" title="版本怎么选"></a>版本怎么选</h2><p>截至 <code>2026-05</code>，ROS 2 仍然建议按 Ubuntu 版本选择发行版，不要随意混装。</p><div class="md-table-scroll"><table><thead><tr><th>系统</th><th>建议发行版</th><th>说明</th></tr></thead><tbody><tr><td>Ubuntu 26.04</td><td><code>Lyrical</code></td><td>最新稳定发行版，适合新机器和新项目</td></tr><tr><td>Ubuntu 24.04</td><td><code>Jazzy</code></td><td>长期维护周期较长，资料和生态更稳</td></tr><tr><td>Ubuntu 22.04</td><td><code>Humble</code></td><td>老项目常见，适合维护已有环境</td></tr><tr><td>Ubuntu 20.04</td><td>ROS 1 Noetic / 旧 ROS 2</td><td>不建议新项目继续从这里开始</td></tr></tbody></table></div><p>新手如果电脑是 Ubuntu 24.04，优先选择 <code>Jazzy</code>；如果已经是 Ubuntu 26.04，再选择 <code>Lyrical</code>。</p><h2 id="ROS-2-核心概念"><a href="#ROS-2-核心概念" class="headerlink" title="ROS 2 核心概念"></a>ROS 2 核心概念</h2><div class="md-table-scroll"><table><thead><tr><th>概念</th><th>作用</th></tr></thead><tbody><tr><td>Node</td><td>节点，一个独立功能进程或组件</td></tr><tr><td>Topic</td><td>话题，发布订阅模型，适合连续数据</td></tr><tr><td>Service</td><td>服务，请求响应模型，适合一次性调用</td></tr><tr><td>Action</td><td>动作，适合耗时任务，比如导航到目标点</td></tr><tr><td>Parameter</td><td>参数，运行时配置</td></tr><tr><td>Message</td><td>消息类型，定义话题传输的数据结构</td></tr><tr><td>Launch</td><td>启动文件，一次启动多个节点和配置</td></tr><tr><td>Bag</td><td>数据录制与回放，常用于调试传感器数据</td></tr><tr><td>tf2</td><td>坐标变换系统，维护机器人各坐标系关系</td></tr></tbody></table></div><p>可以把 ROS 2 理解成：很多小节点通过 Topic、Service、Action 互相通信，再由 Launch 统一启动，由 Bag 记录现场数据，由 tf2 管理坐标关系。</p><h2 id="Ubuntu-24-04-安装-Jazzy"><a href="#Ubuntu-24-04-安装-Jazzy" class="headerlink" title="Ubuntu 24.04 安装 Jazzy"></a>Ubuntu 24.04 安装 Jazzy</h2><p>先确认系统版本：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsb_release -a</span><br></pre></td></tr></tbody></table></figure><p>配置 UTF-8 环境：</p><figure class="highlight bash"><table><tbody><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">locale</span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install locales</span><br><span class="line"><span class="built_in">sudo</span> locale-gen en_US en_US.UTF-8</span><br><span class="line"><span class="built_in">sudo</span> update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8</span><br><span class="line"><span class="built_in">export</span> LANG=en_US.UTF-8</span><br><span class="line">locale</span><br></pre></td></tr></tbody></table></figure><p>启用基础软件源，并添加 ROS 2 apt 源：</p><figure class="highlight bash"><table><tbody><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="built_in">sudo</span> apt install software-properties-common</span><br><span class="line"><span class="built_in">sudo</span> add-apt-repository universe</span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install curl</span><br><span class="line"></span><br><span class="line"><span class="built_in">export</span> ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F <span class="string">"tag_name"</span> | awk -F<span class="string">'"'</span> <span class="string">'{print $4}'</span>)</span><br><span class="line">curl -L -o /tmp/ros2-apt-source.deb <span class="string">"https://github.com/ros-infrastructure/ros-apt-source/releases/download/<span class="variable">${ROS_APT_SOURCE_VERSION}</span>/ros2-apt-source_<span class="variable">${ROS_APT_SOURCE_VERSION}</span>.<span class="subst">$(. /etc/os-release &amp;&amp; echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})</span>_all.deb"</span></span><br><span class="line"><span class="built_in">sudo</span> dpkg -i /tmp/ros2-apt-source.deb</span><br></pre></td></tr></tbody></table></figure><p>安装 ROS 2：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt upgrade</span><br><span class="line"><span class="built_in">sudo</span> apt install ros-jazzy-desktop</span><br><span class="line"><span class="built_in">sudo</span> apt install ros-dev-tools</span><br></pre></td></tr></tbody></table></figure><p><code>desktop</code> 包包含 RViz、示例和常用桌面工具。服务器环境可以只装基础包：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install ros-jazzy-ros-base</span><br></pre></td></tr></tbody></table></figure><h2 id="Ubuntu-26-04-安装-Lyrical"><a href="#Ubuntu-26-04-安装-Lyrical" class="headerlink" title="Ubuntu 26.04 安装 Lyrical"></a>Ubuntu 26.04 安装 Lyrical</h2><p>如果使用 Ubuntu 26.04，把上面的安装包名称换成 <code>lyrical</code>：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install ros-lyrical-desktop</span><br><span class="line"><span class="built_in">sudo</span> apt install ros-dev-tools</span><br></pre></td></tr></tbody></table></figure><p>环境文件也要对应切换：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">source</span> /opt/ros/lyrical/setup.bash</span><br></pre></td></tr></tbody></table></figure><p>不要在一个终端里同时 source 多个发行版，否则后面排查会非常痛苦。</p><h2 id="配置环境变量"><a href="#配置环境变量" class="headerlink" title="配置环境变量"></a>配置环境变量</h2><p>临时生效：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">source</span> /opt/ros/jazzy/setup.bash</span><br></pre></td></tr></tbody></table></figure><p>长期生效：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">"source /opt/ros/jazzy/setup.bash"</span> &gt;&gt; ~/.bashrc</span><br><span class="line"><span class="built_in">source</span> ~/.bashrc</span><br></pre></td></tr></tbody></table></figure><p>如果你使用 <code>zsh</code>：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">"source /opt/ros/jazzy/setup.zsh"</span> &gt;&gt; ~/.zshrc</span><br><span class="line"><span class="built_in">source</span> ~/.zshrc</span><br></pre></td></tr></tbody></table></figure><p>检查命令是否可用：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ros2 --<span class="built_in">help</span></span><br><span class="line">ros2 doctor</span><br></pre></td></tr></tbody></table></figure><p>首次使用依赖解析工具时，初始化 <code>rosdep</code>：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> rosdep init</span><br><span class="line">rosdep update</span><br></pre></td></tr></tbody></table></figure><p>如果提示已经初始化过，后续只需要执行 <code>rosdep update</code>。</p><h2 id="跑通第一个示例"><a href="#跑通第一个示例" class="headerlink" title="跑通第一个示例"></a>跑通第一个示例</h2><p>开两个终端，每个终端都要先 source 环境。</p><p>终端一：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">source</span> /opt/ros/jazzy/setup.bash</span><br><span class="line">ros2 run demo_nodes_cpp talker</span><br></pre></td></tr></tbody></table></figure><p>终端二：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">source</span> /opt/ros/jazzy/setup.bash</span><br><span class="line">ros2 run demo_nodes_py listener</span><br></pre></td></tr></tbody></table></figure><p>如果 <code>talker</code> 不断输出 <code>Publishing</code>，<code>listener</code> 不断输出 <code>I heard</code>，说明 C++ 和 Python 的基础通信都跑通了。</p><h2 id="常用命令速查"><a href="#常用命令速查" class="headerlink" title="常用命令速查"></a>常用命令速查</h2><div class="md-table-scroll"><table><thead><tr><th>命令</th><th>作用</th></tr></thead><tbody><tr><td><code>ros2 node list</code></td><td>查看节点</td></tr><tr><td><code>ros2 node info /node_name</code></td><td>查看节点信息</td></tr><tr><td><code>ros2 topic list</code></td><td>查看话题</td></tr><tr><td><code>ros2 topic echo /topic</code></td><td>打印话题数据</td></tr><tr><td><code>ros2 topic info /topic</code></td><td>查看话题类型和连接数量</td></tr><tr><td><code>ros2 interface show std_msgs/msg/String</code></td><td>查看消息结构</td></tr><tr><td><code>ros2 service list</code></td><td>查看服务</td></tr><tr><td><code>ros2 service call /service type "{}"</code></td><td>调用服务</td></tr><tr><td><code>ros2 action list</code></td><td>查看动作</td></tr><tr><td><code>ros2 param list</code></td><td>查看参数</td></tr><tr><td><code>ros2 param get /node param_name</code></td><td>获取参数</td></tr><tr><td><code>ros2 launch package file.launch.py</code></td><td>启动 launch 文件</td></tr><tr><td><code>ros2 bag record /topic</code></td><td>录制话题</td></tr><tr><td><code>ros2 bag play bag_dir</code></td><td>回放 bag</td></tr></tbody></table></div><h2 id="创建工作空间"><a href="#创建工作空间" class="headerlink" title="创建工作空间"></a>创建工作空间</h2><p>ROS 2 项目通常放在 workspace 里，源码放进 <code>src</code>。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p ~/ros2_ws/src</span><br><span class="line"><span class="built_in">cd</span> ~/ros2_ws</span><br></pre></td></tr></tbody></table></figure><p>初始化依赖：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">rosdep update</span><br><span class="line">rosdep install -i --from-path src --rosdistro jazzy -y</span><br></pre></td></tr></tbody></table></figure><p>构建：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">colcon build</span><br></pre></td></tr></tbody></table></figure><p>让当前终端识别这个工作空间：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">source</span> install/setup.bash</span><br></pre></td></tr></tbody></table></figure><p>长期开发时，可以只把系统 ROS 写进 <code>~/.bashrc</code>，项目 workspace 的 <code>install/setup.bash</code> 在进入项目时手动 source。这样不同项目之间不容易互相污染。</p><h2 id="创建第一个包"><a href="#创建第一个包" class="headerlink" title="创建第一个包"></a>创建第一个包</h2><p>进入 <code>src</code>：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/ros2_ws/src</span><br></pre></td></tr></tbody></table></figure><p>创建 Python 包：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name hello_node leroi_demo</span><br></pre></td></tr></tbody></table></figure><p>回到工作空间根目录构建：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/ros2_ws</span><br><span class="line">colcon build --packages-select leroi_demo</span><br><span class="line"><span class="built_in">source</span> install/setup.bash</span><br></pre></td></tr></tbody></table></figure><p>运行节点：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ros2 run leroi_demo hello_node</span><br></pre></td></tr></tbody></table></figure><p>如果提示找不到包或找不到可执行文件，优先检查：</p><ul><li>是否在 workspace 根目录执行了 <code>colcon build</code>。</li><li>是否执行了 <code>source install/setup.bash</code>。</li><li>包名、节点名是否拼写一致。</li><li><code>setup.py</code> 或 <code>CMakeLists.txt</code> 是否正确安装了可执行入口。</li></ul><h2 id="发布订阅怎么理解"><a href="#发布订阅怎么理解" class="headerlink" title="发布订阅怎么理解"></a>发布订阅怎么理解</h2><p>Topic 是 ROS 里最常用的通信方式。</p><p>一个节点发布数据：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">camera_node -&gt; /camera/image -&gt; detect_node</span><br></pre></td></tr></tbody></table></figure><p>另一个节点订阅数据：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">detect_node -&gt; /target/pose -&gt; controller_node</span><br></pre></td></tr></tbody></table></figure><p>关键点：</p><ul><li>发布者和订阅者的话题名必须一致。</li><li>消息类型必须一致。</li><li>多个节点可以同时订阅同一个话题。</li><li>Topic 适合连续数据，比如图像、雷达、里程计、状态。</li><li>Service 更适合一次性请求，比如“保存地图”。</li><li>Action 更适合长任务，比如“移动到目标点”。</li></ul><h2 id="turtlesim-入门调试"><a href="#turtlesim-入门调试" class="headerlink" title="turtlesim 入门调试"></a>turtlesim 入门调试</h2><p>安装桌面版后通常可以直接跑 turtlesim：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ros2 run turtlesim turtlesim_node</span><br></pre></td></tr></tbody></table></figure><p>另开一个终端控制：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ros2 run turtlesim turtle_teleop_key</span><br></pre></td></tr></tbody></table></figure><p>查看话题：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ros2 topic list</span><br></pre></td></tr></tbody></table></figure><p>查看小乌龟位姿：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ros2 topic <span class="built_in">echo</span> /turtle1/pose</span><br></pre></td></tr></tbody></table></figure><p>这套流程适合理解节点、话题、键盘控制、实时状态输出。</p><h2 id="常见报错"><a href="#常见报错" class="headerlink" title="常见报错"></a>常见报错</h2><h3 id="ros2-command-not-found"><a href="#ros2-command-not-found" class="headerlink" title="ros2: command not found"></a><code>ros2: command not found</code></h3><p>环境没有 source。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">source</span> /opt/ros/jazzy/setup.bash</span><br></pre></td></tr></tbody></table></figure><p>如果每次打开终端都丢失，检查 <code>~/.bashrc</code> 或 <code>~/.zshrc</code>。</p><h3 id="Unable-to-locate-package-ros-jazzy-desktop"><a href="#Unable-to-locate-package-ros-jazzy-desktop" class="headerlink" title="Unable to locate package ros-jazzy-desktop"></a><code>Unable to locate package ros-jazzy-desktop</code></h3><p>常见原因：</p><ul><li>Ubuntu 版本和 ROS 发行版不匹配。</li><li>ROS apt 源没有添加成功。</li><li>没有执行 <code>sudo apt update</code>。</li><li><code>universe</code> 仓库没有启用。</li><li>网络无法访问相关软件源。</li></ul><p>先确认：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">lsb_release -a</span><br><span class="line">apt-cache search ros-jazzy-desktop</span><br></pre></td></tr></tbody></table></figure><h3 id="colcon-command-not-found"><a href="#colcon-command-not-found" class="headerlink" title="colcon: command not found"></a><code>colcon: command not found</code></h3><p>开发工具未安装：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install ros-dev-tools</span><br></pre></td></tr></tbody></table></figure><p>如果仍不可用，再补：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install python3-colcon-common-extensions</span><br></pre></td></tr></tbody></table></figure><h3 id="package-not-found"><a href="#package-not-found" class="headerlink" title="package not found"></a><code>package not found</code></h3><p>通常是 workspace 没有 source：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/ros2_ws</span><br><span class="line"><span class="built_in">source</span> install/setup.bash</span><br></pre></td></tr></tbody></table></figure><p>如果刚修改过代码，重新构建：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">colcon build --packages-select package_name</span><br><span class="line"><span class="built_in">source</span> install/setup.bash</span><br></pre></td></tr></tbody></table></figure><h3 id="两台机器节点互相发现不了"><a href="#两台机器节点互相发现不了" class="headerlink" title="两台机器节点互相发现不了"></a>两台机器节点互相发现不了</h3><p>排查方向：</p><ul><li>两台机器是否在同一网络。</li><li>防火墙是否阻止 DDS 通信。</li><li><code>ROS_DOMAIN_ID</code> 是否一致。</li><li>是否跨 ROS 2 发行版混用。</li><li>虚拟机、Docker、VPN 是否影响组播。</li></ul><p>可以先在两台机器分别查看：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="variable">$ROS_DOMAIN_ID</span></span><br><span class="line">ros2 node list</span><br></pre></td></tr></tbody></table></figure><p>同一项目里建议显式设置一个固定域：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> ROS_DOMAIN_ID=12</span><br></pre></td></tr></tbody></table></figure><h3 id="RViz-或-turtlesim-无法打开"><a href="#RViz-或-turtlesim-无法打开" class="headerlink" title="RViz 或 turtlesim 无法打开"></a>RViz 或 turtlesim 无法打开</h3><p>常见原因：</p><ul><li>安装的是 <code>ros-base</code>，没有桌面工具。</li><li>SSH 没有开启 X11 转发。</li><li>虚拟机图形加速异常。</li><li>Docker 没有透传显示环境。</li></ul><p>本机学习建议安装：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install ros-jazzy-desktop</span><br></pre></td></tr></tbody></table></figure><p>服务器只跑节点时不一定需要桌面工具。</p><h2 id="学习顺序"><a href="#学习顺序" class="headerlink" title="学习顺序"></a>学习顺序</h2><ol><li>安装 ROS 2 并跑通 <code>talker/listener</code>。</li><li>使用 <code>turtlesim</code> 理解节点、话题和命令行。</li><li>建立自己的 <code>ros2_ws</code>。</li><li>创建第一个 Python 或 C++ 包。</li><li>写一个 publisher 和 subscriber。</li><li>学会 launch 文件，一次启动多个节点。</li><li>学会 rosbag，录制和回放现场数据。</li><li>学 tf2、URDF、RViz，再进入机器人模型、仿真和导航。</li></ol><h2 id="站内历史文章"><a href="#站内历史文章" class="headerlink" title="站内历史文章"></a>站内历史文章</h2><ul><li><a href="/posts/82352961/">安装并配置ROS环境</a></li><li><a href="/posts/82715133/">ROS相关资料</a></li><li><a href="/posts/82767904/">ROS相关资料更新</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/iot-ros2-quickstart/</id>
    <link href="https://blogs.microcyan.com/posts/iot-ros2-quickstart/"/>
    <published>2026-05-25T01:34:00.000Z</published>
    <summary>ROS 2 快速入门，整理发行版选择、Ubuntu 安装、环境变量、常用命令、工作空间、包创建、节点通信和常见报错。</summary>
    <title>ROS 2 快速入门</title>
    <updated>2026-09-08T10:34:43.299Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="Kubernetes" scheme="https://blogs.microcyan.com/tags/Kubernetes/"/>
    <category term="服务器运维" scheme="https://blogs.microcyan.com/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%90%E7%BB%B4/"/>
    <category term="部署" scheme="https://blogs.microcyan.com/tags/%E9%83%A8%E7%BD%B2/"/>
    <content>
      <![CDATA[<!-- more --><p>Kubernetes 适合管理多节点、多服务、可扩缩容的容器应用。小项目不一定需要 K8s；如果只是单机部署，Docker Compose、1Panel 或普通 systemd 可能更简单。</p><h2 id="基础组件"><a href="#基础组件" class="headerlink" title="基础组件"></a>基础组件</h2><div class="md-table-scroll"><table><thead><tr><th>组件</th><th>作用</th></tr></thead><tbody><tr><td><code>kubeadm</code></td><td>初始化或加入集群</td></tr><tr><td><code>kubelet</code></td><td>节点上的核心代理，负责运行 Pod</td></tr><tr><td><code>kubectl</code></td><td>命令行管理工具</td></tr><tr><td><code>containerd</code></td><td>常用容器运行时</td></tr><tr><td>CNI</td><td>Pod 网络插件，例如 Calico、Flannel</td></tr><tr><td>Ingress Controller</td><td><code>HTTP/HTTPS</code> 入口控制</td></tr></tbody></table></div><p>Kubernetes 现在通过 CRI 对接容器运行时。Docker Engine 本身不直接实现 CRI，如果要用 Docker，需要额外组件；生产和学习环境更常见的选择是 <code>containerd</code>。</p><h2 id="节点准备"><a href="#节点准备" class="headerlink" title="节点准备"></a>节点准备</h2><p>每个节点都建议先检查：</p><figure class="highlight bash"><table><tbody><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">hostnamectl</span><br><span class="line">free -h</span><br><span class="line"><span class="built_in">df</span> -h</span><br><span class="line">ip addr</span><br><span class="line">timedatectl</span><br></pre></td></tr></tbody></table></figure><p>关闭 swap：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> swapoff -a</span><br></pre></td></tr></tbody></table></figure><p>还要从 <code>/etc/fstab</code> 移除或注释 swap 挂载，否则重启后会恢复。</p><p>加载内核模块：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> &lt;&lt;<span class="string">EOF | sudo tee /etc/modules-load.d/k8s.conf</span></span><br><span class="line"><span class="string">overlay</span></span><br><span class="line"><span class="string">br_netfilter</span></span><br><span class="line"><span class="string">EOF</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> modprobe overlay</span><br><span class="line"><span class="built_in">sudo</span> modprobe br_netfilter</span><br></pre></td></tr></tbody></table></figure><p>内核参数：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> &lt;&lt;<span class="string">EOF | sudo tee /etc/sysctl.d/k8s.conf</span></span><br><span class="line"><span class="string">net.bridge.bridge-nf-call-iptables = 1</span></span><br><span class="line"><span class="string">net.bridge.bridge-nf-call-ip6tables = 1</span></span><br><span class="line"><span class="string">net.ipv4.ip_forward = 1</span></span><br><span class="line"><span class="string">EOF</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> sysctl --system</span><br></pre></td></tr></tbody></table></figure><h2 id="containerd-检查"><a href="#containerd-检查" class="headerlink" title="containerd 检查"></a>containerd 检查</h2><p>安装并启动后检查：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl status containerd</span><br><span class="line"><span class="built_in">sudo</span> crictl info</span><br></pre></td></tr></tbody></table></figure><p>如果 kubelet 和 containerd 的 cgroup driver 不一致，集群初始化很容易失败。systemd 系统通常建议使用 <code>SystemdCgroup = true</code>。</p><p>常见配置位置：</p><figure class="highlight txt"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/etc/containerd/config.toml</span><br></pre></td></tr></tbody></table></figure><p>改完后：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart containerd</span><br></pre></td></tr></tbody></table></figure><h2 id="安装-kubeadm、kubelet、kubectl"><a href="#安装-kubeadm、kubelet、kubectl" class="headerlink" title="安装 kubeadm、kubelet、kubectl"></a>安装 kubeadm、kubelet、kubectl</h2><p>官方仓库按 Kubernetes 小版本区分。示例使用 <code>v1.36</code>，实际要按官方文档和目标集群版本替换。</p><figure class="highlight bash"><table><tbody><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">KUBE_VERSION=v1.36</span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install -y apt-transport-https ca-certificates curl gpg</span><br><span class="line"><span class="built_in">sudo</span> install -m 0755 -d /etc/apt/keyrings</span><br><span class="line">curl -fsSL https://pkgs.k8s.io/core:/stable:/<span class="variable">${KUBE_VERSION}</span>/deb/Release.key | <span class="built_in">sudo</span> gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg</span><br><span class="line"><span class="built_in">echo</span> <span class="string">"deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/<span class="variable">${KUBE_VERSION}</span>/deb/ /"</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/sources.list.d/kubernetes.list</span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install -y kubelet kubeadm kubectl</span><br><span class="line"><span class="built_in">sudo</span> apt-mark hold kubelet kubeadm kubectl</span><br></pre></td></tr></tbody></table></figure><p>如果你用的不是 <code>Debian/Ubuntu</code>，按官方文档切换到对应系统的安装方式。</p><h2 id="初始化控制平面"><a href="#初始化控制平面" class="headerlink" title="初始化控制平面"></a>初始化控制平面</h2><p>示例：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> kubeadm init --pod-network-cidr=10.244.0.0/16</span><br></pre></td></tr></tbody></table></figure><p><code>--pod-network-cidr</code> 要和 CNI 插件匹配。Flannel、Calico 的推荐网段不一定一样，不要随手复制。</p><p>初始化完成后配置 kubectl：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p <span class="variable">$HOME</span>/.kube</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">cp</span> -i /etc/kubernetes/admin.conf <span class="variable">$HOME</span>/.kube/config</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> $(<span class="built_in">id</span> -u):$(<span class="built_in">id</span> -g) <span class="variable">$HOME</span>/.kube/config</span><br></pre></td></tr></tbody></table></figure><p>查看节点：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">kubectl get nodes</span><br><span class="line">kubectl get pods -A</span><br></pre></td></tr></tbody></table></figure><p>安装 CNI 后，节点才会从 <code>NotReady</code> 变成 <code>Ready</code>。</p><h2 id="工作节点加入"><a href="#工作节点加入" class="headerlink" title="工作节点加入"></a>工作节点加入</h2><p>控制平面初始化完成后会输出 <code>kubeadm join</code> 命令。忘记后可以重新生成：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubeadm token create --print-join-command</span><br></pre></td></tr></tbody></table></figure><p>在工作节点执行输出的 join 命令。</p><h2 id="常用-kubectl"><a href="#常用-kubectl" class="headerlink" title="常用 kubectl"></a>常用 kubectl</h2><figure class="highlight php"><table><tbody><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">kubectl get nodes -o wide</span><br><span class="line">kubectl get pods -A -o wide</span><br><span class="line">kubectl describe pod pod_name -n <span class="keyword">namespace</span></span><br><span class="line"><span class="title class_">kubectl</span> <span class="title class_">logs</span> <span class="title class_">pod_name</span> -<span class="title class_">n</span> <span class="title class_">namespace</span></span><br><span class="line"><span class="title class_">kubectl</span> <span class="title class_">logs</span> -<span class="title class_">f</span> <span class="title class_">pod_name</span> -<span class="title class_">n</span> <span class="title class_">namespace</span></span><br><span class="line"><span class="title class_">kubectl</span> <span class="title class_">get</span> <span class="title class_">events</span> -<span class="title class_">A</span> --<span class="title class_">sort</span>-<span class="title class_">by</span>=.<span class="title class_">metadata</span>.<span class="title class_">creationTimestamp</span></span><br><span class="line"><span class="title class_">kubectl</span> <span class="title class_">top</span> <span class="title class_">nodes</span></span><br><span class="line"><span class="title class_">kubectl</span> <span class="title class_">top</span> <span class="title class_">pods</span> -<span class="title class_">A</span></span><br></pre></td></tr></tbody></table></figure><p>进入容器：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl <span class="built_in">exec</span> -it pod_name -n namespace -- sh</span><br></pre></td></tr></tbody></table></figure><h2 id="常见报错"><a href="#常见报错" class="headerlink" title="常见报错"></a>常见报错</h2><div class="md-table-scroll"><table><thead><tr><th>报错</th><th>常见原因</th><th>处理方向</th></tr></thead><tbody><tr><td><code>preflight</code> 提示 swap</td><td>swap 未关闭</td><td><code>swapoff -a</code> 并修改 <code>/etc/fstab</code></td></tr><tr><td><code>container runtime is not running</code></td><td>containerd 未启动或 CRI 配置异常</td><td>查 <code>containerd</code> 和 <code>crictl info</code></td></tr><tr><td><code>kubelet is not running</code></td><td>kubelet 配置、cgroup、运行时异常</td><td><code>journalctl -u kubelet</code></td></tr><tr><td>节点 <code>NotReady</code></td><td>CNI 未安装或网络异常</td><td><code>kubectl get pods -A</code>、看 CNI Pod</td></tr><tr><td><code>ImagePullBackOff</code></td><td>镜像名、tag、仓库权限或网络问题</td><td><code>kubectl describe pod</code></td></tr><tr><td><code>CrashLoopBackOff</code></td><td>容器启动后反复崩溃</td><td><code>kubectl logs --previous</code></td></tr><tr><td><code>Pending</code></td><td>资源不足、PVC 未绑定、调度规则不满足</td><td><code>kubectl describe pod</code></td></tr><tr><td>Ingress 不通</td><td>Ingress Controller、Service、DNS、证书配置问题</td><td>从 Pod、Service、Ingress 逐层查</td></tr></tbody></table></div><h2 id="排查顺序"><a href="#排查顺序" class="headerlink" title="排查顺序"></a>排查顺序</h2><ol><li><code>kubectl get nodes</code> 看节点。</li><li><code>kubectl get pods -A</code> 看系统组件。</li><li><code>kubectl describe</code> 看事件。</li><li><code>kubectl logs</code> 看容器日志。</li><li>到节点上看 <code>kubelet</code>、<code>containerd</code>、磁盘和网络。</li><li>最后再改 YAML，不要一上来就重装集群。</li></ol><h2 id="官方入口"><a href="#官方入口" class="headerlink" title="官方入口"></a>官方入口</h2><ul><li><a target="_blank" rel="noopener" href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/">Installing kubeadm</a></li><li><a target="_blank" rel="noopener" href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/">Creating a cluster with kubeadm</a></li><li><a target="_blank" rel="noopener" href="https://kubernetes.io/docs/setup/production-environment/container-runtimes/">Container Runtimes</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/ops-kubernetes/</id>
    <link href="https://blogs.microcyan.com/posts/ops-kubernetes/"/>
    <published>2026-04-30T01:05:00.000Z</published>
    <summary>Kubernetes、k8s、kubeadm、containerd、kubectl、节点状态、Pod 排查和常见报错。</summary>
    <title>Kubernetes 运维</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="Docker" scheme="https://blogs.microcyan.com/tags/Docker/"/>
    <category term="服务器运维" scheme="https://blogs.microcyan.com/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%90%E7%BB%B4/"/>
    <category term="部署" scheme="https://blogs.microcyan.com/tags/%E9%83%A8%E7%BD%B2/"/>
    <content>
      <![CDATA[<!-- more --><p>Docker 适合把应用和依赖打包到容器里运行。运维时重点关注镜像来源、容器状态、端口映射、数据卷、日志、磁盘空间和重启策略。</p><h2 id="Ubuntu-安装示例"><a href="#Ubuntu-安装示例" class="headerlink" title="Ubuntu 安装示例"></a>Ubuntu 安装示例</h2><p>官方推荐通过 Docker APT 仓库安装。下面是 Ubuntu 示例：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install -y ca-certificates curl</span><br><span class="line"><span class="built_in">sudo</span> install -m 0755 -d /etc/apt/keyrings</span><br><span class="line"><span class="built_in">sudo</span> curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> a+r /etc/apt/keyrings/docker.asc</span><br></pre></td></tr></tbody></table></figure><p>添加源：</p><figure class="highlight bash"><table><tbody><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="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/sources.list.d/docker.sources &lt;&lt;<span class="string">EOF</span></span><br><span class="line"><span class="string">Types: deb</span></span><br><span class="line"><span class="string">URIs: https://download.docker.com/linux/ubuntu</span></span><br><span class="line"><span class="string">Suites: $(. /etc/os-release &amp;&amp; echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")</span></span><br><span class="line"><span class="string">Components: stable</span></span><br><span class="line"><span class="string">Architectures: $(dpkg --print-architecture)</span></span><br><span class="line"><span class="string">Signed-By: /etc/apt/keyrings/docker.asc</span></span><br><span class="line"><span class="string">EOF</span></span><br></pre></td></tr></tbody></table></figure><p>安装：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin</span><br></pre></td></tr></tbody></table></figure><p>测试：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> docker run hello-world</span><br></pre></td></tr></tbody></table></figure><p>如果不是 Ubuntu，请按 Docker 官方文档选择对应发行版。</p><h2 id="常用命令"><a href="#常用命令" class="headerlink" title="常用命令"></a>常用命令</h2><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">docker version</span><br><span class="line">docker info</span><br><span class="line">docker ps</span><br><span class="line">docker ps -a</span><br><span class="line">docker images</span><br><span class="line">docker logs -f container_name</span><br><span class="line">docker <span class="built_in">exec</span> -it container_name sh</span><br><span class="line">docker inspect container_name</span><br><span class="line">docker stats</span><br><span class="line">docker system <span class="built_in">df</span></span><br></pre></td></tr></tbody></table></figure><p>停止和删除：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker stop container_name</span><br><span class="line">docker <span class="built_in">rm</span> container_name</span><br><span class="line">docker rmi image_name</span><br></pre></td></tr></tbody></table></figure><p>清理未使用资源：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker system prune</span><br><span class="line">docker image prune</span><br><span class="line">docker volume prune</span><br></pre></td></tr></tbody></table></figure><p>清理前先确认不要误删未挂载但仍有用的数据卷。</p><h2 id="运行容器"><a href="#运行容器" class="headerlink" title="运行容器"></a>运行容器</h2><figure class="highlight bash"><table><tbody><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">docker run -d \</span><br><span class="line">--name web \</span><br><span class="line">--restart unless-stopped \</span><br><span class="line">-p 8080:80 \</span><br><span class="line">-v /data/web:/usr/share/nginx/html \</span><br><span class="line">nginx:alpine</span><br></pre></td></tr></tbody></table></figure><p>关键参数：</p><ul><li><code>-d</code>：后台运行。</li><li><code>--name</code>：容器名称。</li><li><code>--restart unless-stopped</code>：异常退出后自动重启。</li><li><code>-p 8080:80</code>：宿主机端口映射到容器端口。</li><li><code>-v /data/web:/path</code>：挂载数据目录。</li></ul><h2 id="Docker-Compose"><a href="#Docker-Compose" class="headerlink" title="Docker Compose"></a>Docker Compose</h2><p><code>compose.yaml</code> 示例：</p><figure class="highlight yaml"><table><tbody><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="attr">services:</span></span><br><span class="line">  <span class="attr">web:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">nginx:alpine</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">web</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">"8080:80"</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./html:/usr/share/nginx/html</span></span><br></pre></td></tr></tbody></table></figure><p>常用命令：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d</span><br><span class="line">docker compose ps</span><br><span class="line">docker compose logs -f</span><br><span class="line">docker compose restart</span><br><span class="line">docker compose down</span><br></pre></td></tr></tbody></table></figure><p>如果有数据库、对象存储、消息队列，生产环境要把数据目录挂载到明确路径，并做好备份。</p><h2 id="网络和端口"><a href="#网络和端口" class="headerlink" title="网络和端口"></a>网络和端口</h2><p>查看端口：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker port container_name</span><br><span class="line">ss -lntp</span><br></pre></td></tr></tbody></table></figure><p>常见问题：</p><ul><li>宿主机端口已被占用。</li><li>容器内服务没有监听正确端口。</li><li>应用监听 <code>127.0.0.1</code>，容器外访问不到。</li><li>云服务器安全组或系统防火墙没有放行。</li><li>Nginx 反向代理指向了错误端口。</li></ul><h2 id="日志和磁盘"><a href="#日志和磁盘" class="headerlink" title="日志和磁盘"></a>日志和磁盘</h2><p>查看日志：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker logs --<span class="built_in">tail</span> 200 container_name</span><br><span class="line">docker logs -f container_name</span><br></pre></td></tr></tbody></table></figure><p>查看磁盘占用：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker system <span class="built_in">df</span></span><br><span class="line"><span class="built_in">du</span> -sh /var/lib/docker</span><br></pre></td></tr></tbody></table></figure><p>生产环境建议配置日志轮转，避免容器日志把磁盘写满。</p><p><code>/etc/docker/daemon.json</code> 示例：</p><figure class="highlight json"><table><tbody><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="punctuation">{</span></span><br><span class="line">  <span class="attr">"log-driver"</span><span class="punctuation">:</span> <span class="string">"json-file"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"log-opts"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line">    <span class="attr">"max-size"</span><span class="punctuation">:</span> <span class="string">"100m"</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"max-file"</span><span class="punctuation">:</span> <span class="string">"3"</span></span><br><span class="line">  <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><p>重启 Docker：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart docker</span><br></pre></td></tr></tbody></table></figure><h2 id="常见报错"><a href="#常见报错" class="headerlink" title="常见报错"></a>常见报错</h2><div class="md-table-scroll"><table><thead><tr><th>报错</th><th>常见原因</th><th>处理方向</th></tr></thead><tbody><tr><td><code>permission denied while trying to connect to the Docker daemon socket</code></td><td>当前用户不在 docker 组</td><td>使用 <code>sudo</code> 或配置 docker 用户组</td></tr><tr><td><code>Cannot connect to the Docker daemon</code></td><td>Docker 服务未启动</td><td><code>systemctl status docker</code></td></tr><tr><td><code>port is already allocated</code></td><td>端口被占用</td><td>更换端口或停止占用进程</td></tr><tr><td><code>no space left on device</code></td><td>镜像、容器、日志或卷占满磁盘</td><td><code>docker system df</code>、清理无用资源</td></tr><tr><td><code>pull access denied</code></td><td>镜像不存在或无权限</td><td>检查镜像名、登录 registry</td></tr><tr><td><code>manifest unknown</code></td><td>tag 不存在</td><td>换成存在的镜像 tag</td></tr><tr><td><code>exec format error</code></td><td>镜像架构和服务器架构不匹配</td><td>检查 amd64、arm64</td></tr><tr><td>容器一直重启</td><td>启动命令、环境变量、依赖服务异常</td><td><code>docker logs</code> 看真实原因</td></tr></tbody></table></div><h2 id="官方入口"><a href="#官方入口" class="headerlink" title="官方入口"></a>官方入口</h2><ul><li><a target="_blank" rel="noopener" href="https://docs.docker.com/engine/install/">Install Docker Engine</a></li><li><a target="_blank" rel="noopener" href="https://docs.docker.com/engine/install/ubuntu/">Install Docker Engine on Ubuntu</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/ops-docker/</id>
    <link href="https://blogs.microcyan.com/posts/ops-docker/"/>
    <published>2026-04-28T14:01:00.000Z</published>
    <summary>Docker Engine 安装、hello-world 测试、容器、镜像、网络、数据卷、Compose、日志和常见报错。</summary>
    <title>Docker 运维</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="Linux" scheme="https://blogs.microcyan.com/tags/Linux/"/>
    <category term="内网穿透" scheme="https://blogs.microcyan.com/tags/%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F/"/>
    <category term="网络协议" scheme="https://blogs.microcyan.com/tags/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/"/>
    <category term="部署" scheme="https://blogs.microcyan.com/tags/%E9%83%A8%E7%BD%B2/"/>
    <category term="frp" scheme="https://blogs.microcyan.com/tags/frp/"/>
    <content>
      <![CDATA[<!-- more --><p>家里的服务器、办公室电脑或 NAS 通常只能在局域网里访问。需要远程预览一个网站、接收 Webhook，或者连接内网设备的 SSH 时，可以用 frp 在公网服务器和内网设备之间建立转发通道。</p><p>本文从设备准备开始，以一个运行在内网 <code>8080</code> 端口的 Web 服务为例，完成安装、配置、测试与长期运行。主流程使用 Linux，另外补充 Windows 和 macOS 的客户端启动方式。</p><h2 id="frp-的工作流程"><a href="#frp-的工作流程" class="headerlink" title="frp 的工作流程"></a>frp 的工作流程</h2><p>frp 分为两个程序：<code>frps</code> 运行在公网服务器上，负责接收连接和转发流量；<code>frpc</code> 运行在内网，主动连接 <code>frps</code>，再将收到的请求转交给内网应用。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">公网访问者</span><br><span class="line">    |</span><br><span class="line">    | http://公网服务器IP:18080</span><br><span class="line">    v</span><br><span class="line">公网服务器：frps</span><br><span class="line">    | 7000：接收 frpc 连接</span><br><span class="line">    | 18080：Web 代理入口</span><br><span class="line">    |</span><br><span class="line">    | 经 frpc 主动建立的连接转发请求</span><br><span class="line">    v</span><br><span class="line">内网设备：frpc</span><br><span class="line">    |</span><br><span class="line">    | http://127.0.0.1:8080</span><br><span class="line">    v</span><br><span class="line">内网 Web 应用</span><br></pre></td></tr></tbody></table></figure><p>内网没有公网 IP、路由器没有端口映射权限，都不妨碍这个基本流程。前提是内网设备能够向外连接公网服务器，且公网服务器的相关端口可达。<a target="_blank" rel="noopener" href="https://gofrp.org/en/docs/setup/">frp 官方安装与部署说明</a></p><h2 id="需要准备哪些设备"><a href="#需要准备哪些设备" class="headerlink" title="需要准备哪些设备"></a>需要准备哪些设备</h2><div class="md-table-scroll"><table><thead><tr><th>设备或条件</th><th>是否必需</th><th>用途与要求</th></tr></thead><tbody><tr><td>一台有公网 IP 的服务器</td><td>必需</td><td>运行 <code>frps</code>，能够管理端口和防火墙；通常使用云服务器或 VPS</td></tr><tr><td>一台能持续联网的内网设备</td><td>必需</td><td>运行 <code>frpc</code>，可以是电脑、Linux 主机、NAS 或支持对应程序的设备</td></tr><tr><td>内网应用</td><td>必需</td><td>已经可以从 <code>frpc</code> 所在环境访问，如 Web 服务、SSH 服务</td></tr><tr><td>外网测试设备</td><td>建议</td><td>手机关闭 Wi-Fi 后用移动网络验证实际公网访问</td></tr><tr><td>域名</td><td>可选</td><td>直接用公网 IP 和端口测试时不需要；通过常规域名证书提供 HTTPS 时使用</td></tr><tr><td>家用路由器公网 IP</td><td>不需要</td><td>内网客户端主动向外连接，无需做家用路由器端口映射</td></tr></tbody></table></div><p><code>frpc</code> 不一定要和应用装在同一台设备上。例如 <code>frpc</code> 在一台 Linux 主机上，应用在局域网另一台 NAS 上，只要前者能访问后者的 IP 和端口即可。</p><p>硬件配置取决于连接数、吞吐量和应用负载。小规模测试可以从已有的轻量主机开始；实际体验往往更受公网服务器带宽、内网上传带宽和两端线路影响。内网设备休眠、断电或失去网络，转发也会中断。</p><h3 id="本文使用的端口"><a href="#本文使用的端口" class="headerlink" title="本文使用的端口"></a>本文使用的端口</h3><div class="md-table-scroll"><table><thead><tr><th>参数</th><th>示例值</th><th>属于哪台设备</th></tr></thead><tbody><tr><td><code>serverAddr</code></td><td><code>frp.example.com</code></td><td>指向公网服务器的 DNS 记录，可直接改成公网 IP</td></tr><tr><td><code>bindPort</code> / <code>serverPort</code></td><td><code>7000</code></td><td>公网服务器接收内网客户端连接</td></tr><tr><td><code>localPort</code></td><td><code>8080</code></td><td>内网 Web 应用的监听端口</td></tr><tr><td><code>remotePort</code></td><td><code>18080</code></td><td>公网服务器上给访问者使用的转发端口</td></tr></tbody></table></div><p><code>7000</code> 是 frp 连接入口，浏览器访问的是 <code>18080</code>，这两个端口的作用不同。域名、路径和 token 都是示例，应替换为实际环境的值。</p><h2 id="第一步：确认内网服务可以访问"><a href="#第一步：确认内网服务可以访问" class="headerlink" title="第一步：确认内网服务可以访问"></a>第一步：确认内网服务可以访问</h2><p>在准备运行 <code>frpc</code> 的设备上执行：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -i http://127.0.0.1:8080/</span><br></pre></td></tr></tbody></table></figure><p>响应正常后再继续。如果应用在另一台设备上，例如 <code>192.168.1.20</code>，应改为检查：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -i http://192.168.1.20:8080/</span><br></pre></td></tr></tbody></table></figure><p>如果还没有测试应用，且机器已经安装 Python 3，可以在一个没有私密文件的空目录中临时启动静态服务：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p frp-demo</span><br><span class="line"><span class="built_in">cd</span> frp-demo</span><br><span class="line">python3 -m http.server 8080 --<span class="built_in">bind</span> 127.0.0.1</span><br></pre></td></tr></tbody></table></figure><p>测试时保留这个终端。该服务仅用于验证转发，不作为生产 Web 服务；目录中的文件可能被访问者读取。</p><h2 id="第二步：下载并安装-frp"><a href="#第二步：下载并安装-frp" class="headerlink" title="第二步：下载并安装 frp"></a>第二步：下载并安装 frp</h2><p>从 <a target="_blank" rel="noopener" href="https://github.com/fatedier/frp/releases">frp 官方 Releases</a> 下载程序。下面固定使用 <a target="_blank" rel="noopener" href="https://github.com/fatedier/frp/releases/tag/v0.68.0">v0.68.0</a> 演示安装，两端使用相同版本，方便复现配置；它不代表任何时候的最新版本。</p><h3 id="选择操作系统和-CPU-架构"><a href="#选择操作系统和-CPU-架构" class="headerlink" title="选择操作系统和 CPU 架构"></a>选择操作系统和 CPU 架构</h3><p>Linux 和 macOS 可用下面命令查看环境：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">uname</span> -s</span><br><span class="line"><span class="built_in">uname</span> -m</span><br></pre></td></tr></tbody></table></figure><div class="md-table-scroll"><table><thead><tr><th>环境</th><th>发行包标识</th></tr></thead><tbody><tr><td>Linux，<code>x86_64</code></td><td><code>linux_amd64</code></td></tr><tr><td>Linux，<code>aarch64</code> / ARM64</td><td><code>linux_arm64</code></td></tr><tr><td>Intel Mac</td><td><code>darwin_amd64</code></td></tr><tr><td>Apple Silicon Mac</td><td><code>darwin_arm64</code></td></tr><tr><td>Windows x64</td><td><code>windows_amd64</code></td></tr></tbody></table></div><p>NAS、路由器和其他架构应单独确认系统兼容性。下载预编译程序不需要额外安装 Go 开发环境。</p><h3 id="公网服务器安装"><a href="#公网服务器安装" class="headerlink" title="公网服务器安装"></a>公网服务器安装</h3><p>以下命令适用于 Linux x86_64，需要已有 <code>curl</code> 和 <code>tar</code>。ARM64 服务器将 <code>FRP_PLATFORM</code> 改为 <code>linux_arm64</code>：</p><figure class="highlight bash"><table><tbody><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">FRP_VERSION=<span class="string">"0.68.0"</span></span><br><span class="line">FRP_PLATFORM=<span class="string">"linux_amd64"</span></span><br><span class="line">FRP_PACKAGE=<span class="string">"frp_<span class="variable">${FRP_VERSION}</span>_<span class="variable">${FRP_PLATFORM}</span>"</span></span><br><span class="line"></span><br><span class="line">curl -fL --retry 3 \</span><br><span class="line">  <span class="string">"https://github.com/fatedier/frp/releases/download/v<span class="variable">${FRP_VERSION}</span>/<span class="variable">${FRP_PACKAGE}</span>.tar.gz"</span> \</span><br><span class="line">  -o <span class="string">"<span class="variable">${FRP_PACKAGE}</span>.tar.gz"</span></span><br><span class="line">tar -xzf <span class="string">"<span class="variable">${FRP_PACKAGE}</span>.tar.gz"</span></span><br><span class="line"><span class="built_in">cd</span> <span class="string">"<span class="variable">${FRP_PACKAGE}</span>"</span></span><br><span class="line"></span><br><span class="line">./frps --version</span><br><span class="line"><span class="built_in">sudo</span> install -m 755 frps /usr/local/bin/frps</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> -p /etc/frp</span><br></pre></td></tr></tbody></table></figure><h3 id="内网客户端安装"><a href="#内网客户端安装" class="headerlink" title="内网客户端安装"></a>内网客户端安装</h3><p>在内网 Linux 设备上按自身架构执行同样的下载与解压步骤，然后在解压目录安装客户端：</p><figure class="highlight bash"><table><tbody><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">./frpc --version</span><br><span class="line"><span class="built_in">sudo</span> install -m 755 frpc /usr/local/bin/frpc</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> -p /etc/frp</span><br></pre></td></tr></tbody></table></figure><p>macOS 下载对应的 <code>darwin</code> 包，解压后可以直接运行 <code>./frpc</code>；Windows 解压对应的 ZIP 包，在 PowerShell 中使用 <code>.\frpc.exe</code>。后文的 <code>/etc/frp/frpc.toml</code> 是 Linux 路径，其他系统可把配置放在解压目录中。</p><h2 id="第三步：配置公网服务器-frps"><a href="#第三步：配置公网服务器-frps" class="headerlink" title="第三步：配置公网服务器 frps"></a>第三步：配置公网服务器 frps</h2><p>先生成一个随机 token，用作两端连接认证：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openssl rand -hex 32</span><br></pre></td></tr></tbody></table></figure><p>将生成的值保存在自己的配置中，两端必须一致。frp token 验证的是客户端接入身份，不会给公网 Web 应用自动增加登录认证。<a target="_blank" rel="noopener" href="https://gofrp.org/en/docs/features/common/authentication/">frp 认证机制</a></p><p>在公网服务器上创建 <code>/etc/frp/frps.toml</code>：</p><figure class="highlight toml"><table><tbody><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="attr">bindAddr</span> = <span class="string">"0.0.0.0"</span></span><br><span class="line"><span class="attr">bindPort</span> = <span class="number">7000</span></span><br><span class="line"><span class="attr">proxyBindAddr</span> = <span class="string">"0.0.0.0"</span></span><br><span class="line"></span><br><span class="line"><span class="attr">auth.method</span> = <span class="string">"token"</span></span><br><span class="line"><span class="attr">auth.token</span> = <span class="string">"REPLACE_WITH_YOUR_RANDOM_TOKEN"</span></span><br><span class="line"><span class="attr">transport.tls.force</span> = <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">allowPorts</span> = [</span><br><span class="line">  { single = <span class="number">18080</span> }</span><br><span class="line">]</span><br></pre></td></tr></tbody></table></figure><p>这份配置允许客户端申请 <code>18080</code> 作为公网转发端口。<code>allowPorts</code> 只是 frp 的端口许可清单，不会自动修改云安全组或系统防火墙。<a target="_blank" rel="noopener" href="https://gofrp.org/en/docs/reference/server-configures/">frps 配置参考</a></p><p>测试阶段，应在安全组和防火墙中允许 TCP <code>7000</code> 与 TCP <code>18080</code>：前者供内网客户端连接，后者供外部测试者访问。能固定来源地址时按来源放行；不要因此清空防火墙规则。</p><p>检查配置，再前台运行：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">frps verify -c /etc/frp/frps.toml</span><br><span class="line">frps -c /etc/frp/frps.toml</span><br></pre></td></tr></tbody></table></figure><p>保留该终端，接下来配置内网客户端。</p><h2 id="第四步：配置内网客户端-frpc"><a href="#第四步：配置内网客户端-frpc" class="headerlink" title="第四步：配置内网客户端 frpc"></a>第四步：配置内网客户端 frpc</h2><p>在内网 Linux 设备上创建 <code>/etc/frp/frpc.toml</code>：</p><figure class="highlight toml"><table><tbody><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="attr">serverAddr</span> = <span class="string">"frp.example.com"</span></span><br><span class="line"><span class="attr">serverPort</span> = <span class="number">7000</span></span><br><span class="line"></span><br><span class="line"><span class="attr">auth.method</span> = <span class="string">"token"</span></span><br><span class="line"><span class="attr">auth.token</span> = <span class="string">"REPLACE_WITH_YOUR_RANDOM_TOKEN"</span></span><br><span class="line"><span class="attr">transport.tls.enable</span> = <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="section">[[proxies]]</span></span><br><span class="line"><span class="attr">name</span> = <span class="string">"home-web"</span></span><br><span class="line"><span class="attr">type</span> = <span class="string">"tcp"</span></span><br><span class="line"><span class="attr">localIP</span> = <span class="string">"127.0.0.1"</span></span><br><span class="line"><span class="attr">localPort</span> = <span class="number">8080</span></span><br><span class="line"><span class="attr">remotePort</span> = <span class="number">18080</span></span><br></pre></td></tr></tbody></table></figure><p>替换 <code>serverAddr</code> 和 token。如果应用在另一台局域网设备上，把 <code>localIP</code> 改成那台设备的地址，例如 <code>192.168.1.20</code>。</p><p><code>frp.example.com</code> 需要直接解析到服务器。若 DNS 使用 Cloudflare，此记录应选择 DNS only；普通橙云 HTTP 代理不能直接承载这里 TCP <code>7000</code> 上的 frp 连接。</p><p>Linux 检查并启动：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">frpc verify -c /etc/frp/frpc.toml</span><br><span class="line">frpc -c /etc/frp/frpc.toml</span><br></pre></td></tr></tbody></table></figure><p>macOS 在解压目录中执行：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">./frpc verify -c ./frpc.toml</span><br><span class="line">./frpc -c ./frpc.toml</span><br></pre></td></tr></tbody></table></figure><p>Windows 在解压目录的 PowerShell 中执行：</p><figure class="highlight powershell"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">.\frpc.exe verify <span class="literal">-c</span> .\frpc.toml</span><br><span class="line">.\frpc.exe <span class="literal">-c</span> .\frpc.toml</span><br></pre></td></tr></tbody></table></figure><p>代理名称需要避免冲突。本文使用 TOML 的 <code>[[proxies]]</code> 写法，不能混用旧 INI 教程中的 <code>[common]</code> 和 <code>server_addr</code> 等字段。<a target="_blank" rel="noopener" href="https://gofrp.org/en/docs/features/common/configure/">frp 配置格式与 verify 命令</a></p><h2 id="第五步：验证公网访问"><a href="#第五步：验证公网访问" class="headerlink" title="第五步：验证公网访问"></a>第五步：验证公网访问</h2><p>确认服务端和客户端日志没有认证、连接或代理启动错误，再用外网设备访问：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://公网服务器IP:18080/</span><br></pre></td></tr></tbody></table></figure><p>也可以在外网设备执行，使用真实公网 IP 替换占位符：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -i http://YOUR_SERVER_IP:18080/</span><br></pre></td></tr></tbody></table></figure><p>能看到内网应用的响应，就说明基本穿透链路已经打通。这个测试入口使用 HTTP，不应用来传输密码或其他敏感业务数据；正式 Web 访问可继续配置下面的 HTTPS 入口。</p><p>两端配置中开启的 TLS 保护 <code>frpc</code> 与 <code>frps</code> 之间的连接，不会自动给浏览器入口提供 HTTPS。隧道需要验证服务器身份时，还应配置证书、受信任 CA 和服务名校验，不能只依赖 TLS 开关。<a target="_blank" rel="noopener" href="https://gofrp.org/en/docs/features/common/network/network-tls/">frp TLS 配置说明</a></p><h2 id="第六步：Linux-后台运行与开机自启"><a href="#第六步：Linux-后台运行与开机自启" class="headerlink" title="第六步：Linux 后台运行与开机自启"></a>第六步：Linux 后台运行与开机自启</h2><p>下面适用于使用 systemd 的 Linux，例如常见 Debian、Ubuntu 服务器。先结束前台运行的对应进程，避免新服务启动时端口被占用。</p><h3 id="创建运行用户"><a href="#创建运行用户" class="headerlink" title="创建运行用户"></a>创建运行用户</h3><p>在两台 Linux 设备上分别创建专用用户；若已经存在 <code>frp</code> 用户，无需重复创建：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> useradd --system --user-group --no-create-home --shell /usr/sbin/nologin frp</span><br></pre></td></tr></tbody></table></figure><p>公网服务器设置配置权限：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> root:frp /etc/frp/frps.toml</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 640 /etc/frp/frps.toml</span><br></pre></td></tr></tbody></table></figure><p>内网设备设置客户端配置权限：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> root:frp /etc/frp/frpc.toml</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 640 /etc/frp/frpc.toml</span><br></pre></td></tr></tbody></table></figure><h3 id="公网服务器服务文件"><a href="#公网服务器服务文件" class="headerlink" title="公网服务器服务文件"></a>公网服务器服务文件</h3><p>创建 <code>/etc/systemd/system/frps.service</code>：</p><figure class="highlight ini"><table><tbody><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="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=frp server</span><br><span class="line"><span class="attr">Wants</span>=network-<span class="literal">on</span>line.target</span><br><span class="line"><span class="attr">After</span>=network-<span class="literal">on</span>line.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=simple</span><br><span class="line"><span class="attr">User</span>=frp</span><br><span class="line"><span class="attr">Group</span>=frp</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/local/bin/frps -c /etc/frp/frps.toml</span><br><span class="line"><span class="attr">Restart</span>=<span class="literal">on</span>-failure</span><br><span class="line"><span class="attr">RestartSec</span>=<span class="number">5</span>s</span><br><span class="line"><span class="attr">NoNewPrivileges</span>=<span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></tbody></table></figure><p>启动并设置开机自启：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl daemon-reload</span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> --now frps</span><br><span class="line"><span class="built_in">sudo</span> systemctl status frps --no-pager</span><br></pre></td></tr></tbody></table></figure><h3 id="内网客户端服务文件"><a href="#内网客户端服务文件" class="headerlink" title="内网客户端服务文件"></a>内网客户端服务文件</h3><p>创建 <code>/etc/systemd/system/frpc.service</code>：</p><figure class="highlight ini"><table><tbody><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="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=frp client</span><br><span class="line"><span class="attr">Wants</span>=network-<span class="literal">on</span>line.target</span><br><span class="line"><span class="attr">After</span>=network-<span class="literal">on</span>line.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=simple</span><br><span class="line"><span class="attr">User</span>=frp</span><br><span class="line"><span class="attr">Group</span>=frp</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/local/bin/frpc -c /etc/frp/frpc.toml</span><br><span class="line"><span class="attr">Restart</span>=<span class="literal">on</span>-failure</span><br><span class="line"><span class="attr">RestartSec</span>=<span class="number">5</span>s</span><br><span class="line"><span class="attr">NoNewPrivileges</span>=<span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></tbody></table></figure><p>启动并设置开机自启：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl daemon-reload</span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> --now frpc</span><br><span class="line"><span class="built_in">sudo</span> systemctl status frpc --no-pager</span><br></pre></td></tr></tbody></table></figure><p>分别查看对应设备的日志：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> journalctl -u frps -n 100 --no-pager</span><br><span class="line"><span class="built_in">sudo</span> journalctl -u frpc -n 100 --no-pager</span><br></pre></td></tr></tbody></table></figure><p>修改 TOML 后先执行 <code>verify</code>，再重启对应服务；修改 <code>.service</code> 文件时还需执行 <code>daemon-reload</code>。systemd 使用方式见 <a target="_blank" rel="noopener" href="https://gofrp.org/en/docs/setup/systemd/">frp 官方说明</a>。</p><p>Windows 可使用任务计划程序配置开机运行，并填写程序、配置文件的绝对路径与工作目录；macOS 可使用 launchd。上述 systemd 文件不能直接用于这两个系统。</p><h2 id="可选：域名与-HTTPS-访问"><a href="#可选：域名与-HTTPS-访问" class="headerlink" title="可选：域名与 HTTPS 访问"></a>可选：域名与 HTTPS 访问</h2><p>已有公网 Web 服务转发后，可以在公网服务器上部署 Nginx，让用户通过 <code>https://app.example.com</code> 访问。需要准备域名、对应 DNS 记录、有效证书以及 Nginx；证书获取和续期应由现有证书管理方案负责。</p><h3 id="将转发端口改为本机可见"><a href="#将转发端口改为本机可见" class="headerlink" title="将转发端口改为本机可见"></a>将转发端口改为本机可见</h3><p>在 <code>frps.toml</code> 中把原有字段改成：</p><figure class="highlight toml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">proxyBindAddr</span> = <span class="string">"127.0.0.1"</span></span><br></pre></td></tr></tbody></table></figure><p>验证并重启 <code>frps</code> 后，公网服务器上的 Nginx 仍可访问 <code>127.0.0.1:18080</code>，外部访问者不再直接连接 <code>18080</code>。安全组应允许 HTTPS 入口 TCP <code>443</code>，同时移除之前测试用的公网 <code>18080</code> 放行规则。</p><p>这个设置对该 <code>frps</code> 的代理监听地址生效，不是只影响某一条 Web 代理。若同一实例还承担需要公网直连的其他端口，应单独规划监听方式或拆分实例。</p><h3 id="配置-Nginx"><a href="#配置-Nginx" class="headerlink" title="配置 Nginx"></a>配置 Nginx</h3><p>将 <code>app.example.com</code> 解析到服务器，在 Nginx 的 HTTP 配置中加入：</p><figure class="highlight nginx"><table><tbody><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="section">server</span> {</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">443</span> ssl;</span><br><span class="line">    <span class="attribute">server_name</span> app.example.com;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">ssl_certificate</span> /etc/nginx/certs/app.example.com/fullchain.pem;</span><br><span class="line">    <span class="attribute">ssl_certificate_key</span> /etc/nginx/certs/app.example.com/privkey.pem;</span><br><span class="line"></span><br><span class="line">    <span class="section">location</span> / {</span><br><span class="line">        <span class="attribute">proxy_pass</span> http://127.0.0.1:18080;</span><br><span class="line">        <span class="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>证书路径必须存在且能被 Nginx 读取，不能直接保留示例路径。检查配置后重载：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> nginx -t</span><br><span class="line"><span class="built_in">sudo</span> nginx -s reload</span><br><span class="line">curl -I https://app.example.com/</span><br></pre></td></tr></tbody></table></figure><p>这份配置面向普通 HTTP 应用。WebSocket 还需要配置升级请求头；AI 聊天等 SSE 接口需要检查代理缓冲和超时。相关指令见 <a target="_blank" rel="noopener" href="https://nginx.org/en/docs/http/ngx_http_proxy_module.html">Nginx 代理模块文档</a>。</p><h2 id="可选：转发-SSH"><a href="#可选：转发-SSH" class="headerlink" title="可选：转发 SSH"></a>可选：转发 SSH</h2><p>本节基于前面 <code>proxyBindAddr = "0.0.0.0"</code> 的公网端口方案。若已经改为回环地址，仅增加规则不会让 SSH 端口对公网开放。</p><p>先确认内网设备已经运行 SSH 服务，并在 <code>frps.toml</code> 的原有 <code>allowPorts</code> 中增加端口，不要重复定义整个字段：</p><figure class="highlight toml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">allowPorts</span> = [</span><br><span class="line">  { single = <span class="number">18080</span> },</span><br><span class="line">  { single = <span class="number">6000</span> }</span><br><span class="line">]</span><br></pre></td></tr></tbody></table></figure><p>然后在 <code>frpc.toml</code> 末尾追加代理：</p><figure class="highlight toml"><table><tbody><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="section">[[proxies]]</span></span><br><span class="line"><span class="attr">name</span> = <span class="string">"home-ssh"</span></span><br><span class="line"><span class="attr">type</span> = <span class="string">"tcp"</span></span><br><span class="line"><span class="attr">localIP</span> = <span class="string">"127.0.0.1"</span></span><br><span class="line"><span class="attr">localPort</span> = <span class="number">22</span></span><br><span class="line"><span class="attr">remotePort</span> = <span class="number">6000</span></span><br></pre></td></tr></tbody></table></figure><p>验证并重启两端服务，按访问来源放行服务器 TCP <code>6000</code>，即可连接：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -p 6000 your_user@YOUR_SERVER_IP</span><br></pre></td></tr></tbody></table></figure><p>这里的 <code>your_user</code> 是内网机器的 SSH 账号。使用密钥认证并核对主机指纹；仅供本人或小团队访问的服务，也可以使用需要访问端参与的 STCP。<a target="_blank" rel="noopener" href="https://gofrp.org/en/docs/examples/ssh/">frp SSH 示例</a>、<a target="_blank" rel="noopener" href="https://gofrp.org/en/docs/features/stcp-sudp/">STCP 使用说明</a></p><h2 id="常见问题与排查顺序"><a href="#常见问题与排查顺序" class="headerlink" title="常见问题与排查顺序"></a>常见问题与排查顺序</h2><div class="md-table-scroll"><table><thead><tr><th>现象</th><th>优先检查</th></tr></thead><tbody><tr><td><code>frpc</code> 连接超时</td><td>公网 IP、DNS、7000 端口、安全组、系统防火墙以及内网出站策略</td></tr><tr><td>token 认证失败</td><td>两端值是否完全一致，是否仍保留占位文本，是否加载了正确的配置文件</td></tr><tr><td><code>port already used</code></td><td>转发端口是否被其他程序占用，是否重复启动了前台进程和系统服务</td></tr><tr><td>端口不被允许</td><td><code>remotePort</code> 是否包含在服务端的 <code>allowPorts</code> 中</td></tr><tr><td><code>proxy name ... already in use</code></td><td>多个客户端是否使用了相同代理名称</td></tr><tr><td>客户端在线，但 Web 访问失败</td><td>应用是否启动，<code>localIP</code> 和 <code>localPort</code> 是否正确</td></tr><tr><td>服务器本机可以访问，外网不行</td><td>代理是否只监听回环地址，入口端口和防火墙是否一致</td></tr><tr><td>Docker 中的 frpc 访问不到应用</td><td><code>127.0.0.1</code> 是否错误地指向 frpc 容器本身</td></tr><tr><td>重启后不工作</td><td>二进制与配置的绝对路径、文件权限、服务是否启用和设备是否联网</td></tr><tr><td>登录后跳转到 localhost</td><td>应用外部 URL、可信代理与 HTTPS 识别配置</td></tr></tbody></table></div><p>Docker 场景中，同一自定义网络里的容器可以使用服务名通信；访问宿主机时使用当前平台支持的宿主机地址。宿主机应用若只监听回环地址，容器不一定能够访问，不能仅替换一个主机名就认为网络已经连通。</p><p>排查时按顺序验证每一段：</p><ol><li>在内网客户端环境中用 <code>curl</code> 检查应用。</li><li>查看 <code>frpc</code> 是否成功连接和注册代理。</li><li>在公网服务器用 <code>ss -lntp</code> 检查端口，再访问本机转发入口。</li><li>从外网设备访问公网 IP 和端口。</li><li>最后检查域名、HTTPS 证书和反向代理。</li></ol><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ss -lntp</span><br><span class="line">curl -i http://127.0.0.1:18080/</span><br></pre></td></tr></tbody></table></figure><p>长期运行时，把 token 配置排除在 Git 仓库之外，并使用应用自身的登录和权限控制。临时分享结束后，删除不再需要的代理规则，或停止相应服务并收回端口放行。</p><h2 id="相关阅读"><a href="#相关阅读" class="headerlink" title="相关阅读"></a>相关阅读</h2><ul><li><a href="/posts/ops-nginx/">Nginx 配置</a></li><li><a href="/posts/ops-docker/">Docker 使用文档</a></li><li><a href="/posts/ops-cloudflare-tunnel/">Cloudflare Tunnel 配置与常见问题</a></li><li><a href="/posts/ops-intranet-tunnel-ngrok/">内网穿透与 ngrok 常见问题</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/ops-frp/</id>
    <link href="https://blogs.microcyan.com/posts/ops-frp/"/>
    <published>2026-03-27T03:24:00.000Z</published>
    <summary>从公网服务器和内网设备准备开始，逐步安装 frps 与 frpc，配置 Web 和 SSH 转发、域名 HTTPS、Linux 开机自启，并整理端口、Docker 网络和连接失败的排查方法。</summary>
    <title>frp 内网穿透使用教程：设备准备、安装配置与完整部署流程</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="Docker" scheme="https://blogs.microcyan.com/tags/Docker/"/>
    <category term="服务器运维" scheme="https://blogs.microcyan.com/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%90%E7%BB%B4/"/>
    <category term="部署" scheme="https://blogs.microcyan.com/tags/%E9%83%A8%E7%BD%B2/"/>
    <category term="Cloudflare Tunnel" scheme="https://blogs.microcyan.com/tags/Cloudflare-Tunnel/"/>
    <content>
      <![CDATA[<!-- more --><p>Cloudflare Tunnel 可以让内网服务通过 <code>cloudflared</code> 主动连接到 Cloudflare，再由 Cloudflare 把公网域名流量转发到本地服务。它不需要在路由器上做端口映射，适合内网 Web 服务、测试环境、面板服务和临时项目预览。</p><blockquote><p><strong>官方入口</strong></p><ul><li><a target="_blank" rel="noopener" href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/">Cloudflare Tunnel 文档</a></li><li><a target="_blank" rel="noopener" href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/">cloudflared 下载与安装</a></li></ul></blockquote><h2 id="基本流程"><a href="#基本流程" class="headerlink" title="基本流程"></a>基本流程</h2><ol><li>域名托管到 Cloudflare。</li><li>安装 <code>cloudflared</code>。</li><li>登录 Cloudflare。</li><li>创建 Tunnel。</li><li>将域名路由到 Tunnel。</li><li>配置本地服务映射。</li><li>以服务方式长期运行。</li></ol><h2 id="CLI-创建-Tunnel"><a href="#CLI-创建-Tunnel" class="headerlink" title="CLI 创建 Tunnel"></a>CLI 创建 Tunnel</h2><p>登录：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cloudflared tunnel login</span><br></pre></td></tr></tbody></table></figure><p>创建：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cloudflared tunnel create leroi-app</span><br></pre></td></tr></tbody></table></figure><p>绑定 DNS：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cloudflared tunnel route dns leroi-app app.example.com</span><br></pre></td></tr></tbody></table></figure><p>配置文件示例：</p><figure class="highlight yaml"><table><tbody><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="attr">tunnel:</span> <span class="string">leroi-app</span></span><br><span class="line"><span class="attr">credentials-file:</span> <span class="string">/root/.cloudflared/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json</span></span><br><span class="line"></span><br><span class="line"><span class="attr">ingress:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">hostname:</span> <span class="string">app.example.com</span></span><br><span class="line">    <span class="attr">service:</span> <span class="string">http://localhost:8080</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">service:</span> <span class="string">http_status:404</span></span><br></pre></td></tr></tbody></table></figure><p>运行：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cloudflared tunnel run leroi-app</span><br></pre></td></tr></tbody></table></figure><p>安装为系统服务：</p><figure class="highlight bash"><table><tbody><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">cloudflared service install</span><br><span class="line">systemctl <span class="built_in">enable</span> cloudflared</span><br><span class="line">systemctl start cloudflared</span><br></pre></td></tr></tbody></table></figure><h2 id="Docker-Token-方式"><a href="#Docker-Token-方式" class="headerlink" title="Docker Token 方式"></a>Docker Token 方式</h2><p>如果在 Cloudflare Zero Trust 面板里创建了 Tunnel，可以用 token 启动：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">  --name cloudflared \</span><br><span class="line">  --restart unless-stopped \</span><br><span class="line">  cloudflare/cloudflared:latest \</span><br><span class="line">  tunnel --no-autoupdate run --token YOUR_TUNNEL_TOKEN</span><br></pre></td></tr></tbody></table></figure><p>这种方式简单，但 token 要保管好，不要提交到 Git。</p><h2 id="多个服务映射"><a href="#多个服务映射" class="headerlink" title="多个服务映射"></a>多个服务映射</h2><figure class="highlight yaml"><table><tbody><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="attr">ingress:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">hostname:</span> <span class="string">api.example.com</span></span><br><span class="line">    <span class="attr">service:</span> <span class="string">http://localhost:8000</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">hostname:</span> <span class="string">files.example.com</span></span><br><span class="line">    <span class="attr">service:</span> <span class="string">http://localhost:5244</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">hostname:</span> <span class="string">ssh.example.com</span></span><br><span class="line">    <span class="attr">service:</span> <span class="string">ssh://localhost:22</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">service:</span> <span class="string">http_status:404</span></span><br></pre></td></tr></tbody></table></figure><p>最后一条兜底规则必须存在，否则未匹配请求可能不知道转发到哪里。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="访问出现-1016"><a href="#访问出现-1016" class="headerlink" title="访问出现 1016"></a>访问出现 1016</h3><p>常见原因：</p><ul><li>DNS 记录指向的 Tunnel 不存在。</li><li>Tunnel 没有运行。</li><li>域名绑定到了错误的 Tunnel。</li><li>CLI 创建了 DNS，但本机服务没有启动。</li></ul><p>检查：</p><figure class="highlight bash"><table><tbody><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">cloudflared tunnel list</span><br><span class="line">cloudflared tunnel info leroi-app</span><br><span class="line">systemctl status cloudflared</span><br></pre></td></tr></tbody></table></figure><h3 id="本地服务能访问，公网访问-502"><a href="#本地服务能访问，公网访问-502" class="headerlink" title="本地服务能访问，公网访问 502"></a>本地服务能访问，公网访问 502</h3><p>检查 <code>service</code> 地址是否从 <code>cloudflared</code> 所在机器可访问：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -I http://localhost:8080</span><br></pre></td></tr></tbody></table></figure><p>如果 <code>cloudflared</code> 在 Docker 里，<code>localhost</code> 指的是容器内部。可以改成宿主机地址，或把应用和 <code>cloudflared</code> 放在同一个 Docker 网络。</p><h3 id="WebSocket-不稳定"><a href="#WebSocket-不稳定" class="headerlink" title="WebSocket 不稳定"></a>WebSocket 不稳定</h3><p>优先确认本地服务和反向代理支持 WebSocket：</p><figure class="highlight nginx"><table><tbody><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="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line"><span class="attribute">proxy_set_header</span> Upgrade <span class="variable">$http_upgrade</span>;</span><br><span class="line"><span class="attribute">proxy_set_header</span> Connection <span class="string">"upgrade"</span>;</span><br></pre></td></tr></tbody></table></figure><p>如果中间还有 Nginx，要同时检查 Nginx 超时时间。</p><h3 id="文件上传失败"><a href="#文件上传失败" class="headerlink" title="文件上传失败"></a>文件上传失败</h3><p>排查方向：</p><ul><li>应用本身上传限制。</li><li>Nginx <code>client_max_body_size</code>。</li><li>后端超时时间。</li><li>Cloudflare 计划和产品限制。</li></ul><p>大文件传输不建议完全依赖 Web 页面上传，可以考虑对象存储、分片上传或 WebDAV。</p><h2 id="安全建议"><a href="#安全建议" class="headerlink" title="安全建议"></a>安全建议</h2><ul><li>管理后台不要直接裸露，可以加 Cloudflare Access。</li><li>内网服务只监听 <code>127.0.0.1</code> 或内网地址。</li><li>Tunnel token 和 credentials 文件不要提交到仓库。</li><li>生产服务要监控 <code>cloudflared</code> 进程和日志。</li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/ops-cloudflare-tunnel/</id>
    <link href="https://blogs.microcyan.com/posts/ops-cloudflare-tunnel/"/>
    <published>2026-03-25T13:07:00.000Z</published>
    <summary>Cloudflare Tunnel、cloudflared、本地服务公网访问、DNS 路由、Docker 部署、常见错误和安全建议。</summary>
    <title>Cloudflare Tunnel 配置与常见问题</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="HTTP" scheme="https://blogs.microcyan.com/tags/HTTP/"/>
    <category term="内网穿透" scheme="https://blogs.microcyan.com/tags/%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F/"/>
    <category term="服务器运维" scheme="https://blogs.microcyan.com/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%90%E7%BB%B4/"/>
    <category term="部署" scheme="https://blogs.microcyan.com/tags/%E9%83%A8%E7%BD%B2/"/>
    <content>
      <![CDATA[<!-- more --><p>内网穿透的核心是让公网用户访问内网服务。常见场景包括微信支付回调、公众号消息回调、本地接口联调、远程演示、临时文件服务和远程调试。</p><blockquote><p><strong>官方入口</strong></p><ul><li><a target="_blank" rel="noopener" href="https://ngrok.com/docs/">ngrok 文档</a></li><li><a target="_blank" rel="noopener" href="https://ngrok.com/docs/http/">ngrok HTTP Endpoints</a></li><li><a target="_blank" rel="noopener" href="https://ngrok.com/docs/agent/">ngrok Agent 配置</a></li></ul></blockquote><h2 id="常见方案对比"><a href="#常见方案对比" class="headerlink" title="常见方案对比"></a>常见方案对比</h2><div class="md-table-scroll"><table><thead><tr><th>方案</th><th>适合场景</th><th>特点</th></tr></thead><tbody><tr><td>ngrok</td><td>本地开发、Webhook、临时预览</td><td>上手快，公网地址由平台分配</td></tr><tr><td>Cloudflare Tunnel</td><td>自有域名、长期服务、Web 管理后台</td><td>不需要开入站端口，适合域名托管在 Cloudflare 的项目</td></tr><tr><td>FRP</td><td>自建穿透、内网设备多</td><td>需要自己有公网服务器</td></tr><tr><td>SSH 反向隧道</td><td>临时转发、轻量排查</td><td>依赖 SSH 连接稳定性</td></tr><tr><td>路由器端口映射</td><td>固定公网 IP、家庭或公司网络</td><td>需要公网 IP 和路由器权限</td></tr></tbody></table></div><h2 id="ngrok-HTTP-示例"><a href="#ngrok-HTTP-示例" class="headerlink" title="ngrok HTTP 示例"></a>ngrok HTTP 示例</h2><p>本地服务运行在 <code>8080</code>：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ngrok http 8080</span><br></pre></td></tr></tbody></table></figure><p>指定本地地址：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ngrok http http://localhost:8080</span><br></pre></td></tr></tbody></table></figure><p>用于微信、GitHub、支付回调调试时，把 ngrok 提供的 HTTPS 地址填到平台回调地址里。</p><h2 id="ngrok-TCP-示例"><a href="#ngrok-TCP-示例" class="headerlink" title="ngrok TCP 示例"></a>ngrok TCP 示例</h2><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ngrok tcp 22</span><br></pre></td></tr></tbody></table></figure><p>适合临时暴露 SSH、数据库或其他 TCP 服务。暴露 SSH 时一定要使用密钥登录，禁止弱密码。</p><h2 id="SSH-反向隧道"><a href="#SSH-反向隧道" class="headerlink" title="SSH 反向隧道"></a>SSH 反向隧道</h2><p>如果有一台公网服务器，可以用 SSH 建立反向隧道：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -N -R 9000:localhost:8080 root@server.example.com</span><br></pre></td></tr></tbody></table></figure><p>含义：</p><ul><li>公网服务器监听 <code>9000</code>。</li><li>访问公网服务器 <code>9000</code> 会转发到本机 <code>8080</code>。</li></ul><p>如果想让公网用户访问，还要确保服务器 <code>sshd_config</code> 允许：</p><figure class="highlight txt"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GatewayPorts yes</span><br></pre></td></tr></tbody></table></figure><p>修改后重启 SSH：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl restart sshd</span><br></pre></td></tr></tbody></table></figure><h2 id="用-autossh-保持连接"><a href="#用-autossh-保持连接" class="headerlink" title="用 autossh 保持连接"></a>用 autossh 保持连接</h2><figure class="highlight bash"><table><tbody><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">autossh -M 0 -N \</span><br><span class="line">  -o ServerAliveInterval=30 \</span><br><span class="line">  -o ServerAliveCountMax=3 \</span><br><span class="line">  -R 9000:localhost:8080 \</span><br><span class="line">  root@server.example.com</span><br></pre></td></tr></tbody></table></figure><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="ngrok-地址每次变"><a href="#ngrok-地址每次变" class="headerlink" title="ngrok 地址每次变"></a>ngrok 地址每次变</h3><p>免费临时地址通常会变化。如果需要固定域名，要使用平台提供的固定域名能力，或改用自有域名加 Cloudflare Tunnel。</p><h3 id="回调平台提示-URL-不合法"><a href="#回调平台提示-URL-不合法" class="headerlink" title="回调平台提示 URL 不合法"></a>回调平台提示 URL 不合法</h3><p>检查：</p><ul><li>是否必须使用 HTTPS。</li><li>回调地址是否能公网访问。</li><li>路径是否完整，例如 <code>/api/wechat/notify</code>。</li><li>平台是否要求备案域名或白名单域名。</li></ul><h3 id="本地能访问，公网访问失败"><a href="#本地能访问，公网访问失败" class="headerlink" title="本地能访问，公网访问失败"></a>本地能访问，公网访问失败</h3><p>检查本地服务监听地址：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ss -lntp | grep 8080</span><br></pre></td></tr></tbody></table></figure><p>如果应用只监听在容器内，隧道客户端可能访问不到。Docker 场景要确认端口映射：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker ps</span><br></pre></td></tr></tbody></table></figure><h3 id="穿透后拿不到真实-IP"><a href="#穿透后拿不到真实-IP" class="headerlink" title="穿透后拿不到真实 IP"></a>穿透后拿不到真实 IP</h3><p>隧道服务通常会在请求头里传递来源信息。后端需要读取：</p><figure class="highlight txt"><table><tbody><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">X-Forwarded-For</span><br><span class="line">X-Real-IP</span><br><span class="line">Forwarded</span><br></pre></td></tr></tbody></table></figure><p>但这些请求头不能无条件信任，生产环境要结合可信代理列表。</p><h3 id="WebSocket-连不上"><a href="#WebSocket-连不上" class="headerlink" title="WebSocket 连不上"></a>WebSocket 连不上</h3><p>确认穿透服务支持 WebSocket，并检查本地代理配置。Nginx 反代 WebSocket 时需要：</p><figure class="highlight nginx"><table><tbody><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="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line"><span class="attribute">proxy_set_header</span> Upgrade <span class="variable">$http_upgrade</span>;</span><br><span class="line"><span class="attribute">proxy_set_header</span> Connection <span class="string">"upgrade"</span>;</span><br></pre></td></tr></tbody></table></figure><h2 id="安全建议"><a href="#安全建议" class="headerlink" title="安全建议"></a>安全建议</h2><ul><li>不要长期暴露数据库、Redis、管理后台。</li><li>暴露 SSH 时禁用密码登录，只允许密钥。</li><li>临时调试结束后关闭隧道。</li><li>回调调试环境和生产环境分开。</li><li>不要把 ngrok token、隧道 token 写进公开仓库。</li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/ops-intranet-tunnel-ngrok/</id>
    <link href="https://blogs.microcyan.com/posts/ops-intranet-tunnel-ngrok/"/>
    <published>2026-03-24T00:51:00.000Z</published>
    <summary>内网穿透、ngrok、HTTP/TCP 隧道、Webhook 调试、远程访问、本地开发预览和常见问题整理。</summary>
    <title>内网穿透与 ngrok 常见问题</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="后端开发" scheme="https://blogs.microcyan.com/categories/backend/"/>
    <category term="HTTP" scheme="https://blogs.microcyan.com/tags/HTTP/"/>
    <category term="后端开发" scheme="https://blogs.microcyan.com/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://blogs.microcyan.com/tags/Go/"/>
    <category term="Gin" scheme="https://blogs.microcyan.com/tags/Gin/"/>
    <content>
      <![CDATA[<!-- more --><p>Gin 项目不需要只为了版本号而升级。存量服务更关注稳定性和依赖兼容，新项目则应结合 Go 工具链、部署环境和中间件支持情况选择版本。</p><h2 id="版本选择"><a href="#版本选择" class="headerlink" title="版本选择"></a>版本选择</h2><div class="md-table-scroll"><table><thead><tr><th>版本</th><th>典型场景</th><th>维护重点</th></tr></thead><tbody><tr><td>Gin 1.9</td><td>运行稳定的存量服务</td><td>锁定依赖、补齐测试、逐步升级</td></tr><tr><td>Gin 1.10</td><td>保守维护的常规 API 项目</td><td>参数绑定、中间件顺序、错误处理</td></tr><tr><td>Gin 1.11</td><td>需要较新协议或表单能力的项目</td><td>Go 版本、HTTP/3 链路、第三方中间件</td></tr><tr><td>Gin 1.12</td><td>新项目或较新 Go 环境</td><td>工具链一致性、依赖兼容、部署验证</td></tr></tbody></table></div><p>已经稳定运行的项目不必跨多个版本升级。准备长期迭代的新项目，则应优先选择团队能够持续维护的稳定版本。</p><h2 id="查看和固定版本"><a href="#查看和固定版本" class="headerlink" title="查看和固定版本"></a>查看和固定版本</h2><p>查看当前版本：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go list -m github.com/gin-gonic/gin</span><br></pre></td></tr></tbody></table></figure><p>指定版本后整理依赖：</p><figure class="highlight bash"><table><tbody><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">go get github.com/gin-gonic/gin@v1.12.0</span><br><span class="line">go mod tidy</span><br><span class="line">go <span class="built_in">test</span> ./...</span><br></pre></td></tr></tbody></table></figure><p><code>go.mod</code> 和 <code>go.sum</code> 应提交到版本库。本地、CI 与服务器使用的 Go 版本也要保持一致。</p><h2 id="通用检查清单"><a href="#通用检查清单" class="headerlink" title="通用检查清单"></a>通用检查清单</h2><div class="md-table-scroll"><table><thead><tr><th>检查项</th><th>说明</th></tr></thead><tbody><tr><td>路由</td><td>分组、路径参数和通配符路由是否符合预期</td></tr><tr><td>中间件</td><td>鉴权失败后是否正确 <code>Abort()</code>，执行顺序是否稳定</td></tr><tr><td>参数绑定</td><td>JSON、Query、Form、URI 和文件上传是否覆盖测试</td></tr><tr><td>错误处理</td><td>panic recovery 和统一错误格式是否有效</td></tr><tr><td>代理配置</td><td>真实 IP、可信代理和转发请求头是否正确</td></tr><tr><td>日志</td><td>请求日志与业务错误日志是否能够关联</td></tr></tbody></table></div><h2 id="Gin-1-9-存量项目"><a href="#Gin-1-9-存量项目" class="headerlink" title="Gin 1.9 存量项目"></a>Gin 1.9 存量项目</h2><p>Gin 1.9 常见于运行时间较长的 API 服务。维护时先固定现有 Go 与依赖版本，再回归登录鉴权、参数校验、文件上传和中间件顺序。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">go get github.com/gin-gonic/gin@v1.9.1</span><br><span class="line">go mod tidy</span><br></pre></td></tr></tbody></table></figure><p>如果业务稳定且改动很少，可以继续维护；计划持续迭代时，应建立测试基线后逐个版本升级。</p><h2 id="Gin-1-10-稳定维护"><a href="#Gin-1-10-稳定维护" class="headerlink" title="Gin 1.10 稳定维护"></a>Gin 1.10 稳定维护</h2><p>Gin 1.10 项目重点检查参数绑定、中间件、panic recovery 和日志。不要在升级 Gin 的同时大范围重构业务，否则出现问题时很难区分原因。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">go get github.com/gin-gonic/gin@v1.10.1</span><br><span class="line">go <span class="built_in">test</span> ./...</span><br></pre></td></tr></tbody></table></figure><h2 id="Gin-1-11-协议与兼容性"><a href="#Gin-1-11-协议与兼容性" class="headerlink" title="Gin 1.11 协议与兼容性"></a>Gin 1.11 协议与兼容性</h2><p>使用较新协议和表单能力前，需要确认反向代理、负载均衡、容器网络和客户端是否共同支持。HTTP/3 等能力只有在完整链路生效时才有意义。</p><figure class="highlight bash"><table><tbody><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">go get github.com/gin-gonic/gin@v1.11.0</span><br><span class="line">go mod tidy</span><br><span class="line">go <span class="built_in">test</span> ./...</span><br></pre></td></tr></tbody></table></figure><p>升级后重点回归请求体绑定、文件上传、自定义中间件和错误响应格式。</p><h2 id="Gin-1-12-新项目"><a href="#Gin-1-12-新项目" class="headerlink" title="Gin 1.12 新项目"></a>Gin 1.12 新项目</h2><p>新项目可以使用较新版本，但应先确认 Go 工具链与部署环境满足要求。</p><figure class="highlight go"><table><tbody><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">module example.com/api</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="number">1.25</span></span><br><span class="line"></span><br><span class="line">require github.com/gin-gonic/gin v1<span class="number">.12</span><span class="number">.0</span></span><br></pre></td></tr></tbody></table></figure><p>推荐按职责组织代码：</p><figure class="highlight text"><table><tbody><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">cmd/</span><br><span class="line">api/</span><br><span class="line">internal/</span><br><span class="line">handler/</span><br><span class="line">service/</span><br><span class="line">repository/</span><br><span class="line">middleware/</span><br></pre></td></tr></tbody></table></figure><p>简单项目可以减少分层，但不要把路由、业务逻辑和数据访问全部放进 <code>main.go</code> 或 handler。</p><h2 id="升级步骤"><a href="#升级步骤" class="headerlink" title="升级步骤"></a>升级步骤</h2><ol><li>固定当前依赖并运行完整测试。</li><li>记录核心接口的响应、性能和错误率基线。</li><li>升级到相邻版本并阅读发布说明。</li><li>回归路由、绑定、中间件、上传和错误分支。</li><li>在测试环境观察日志与性能，再逐步发布。</li></ol><p>具体路由、中间件和统一响应写法可继续阅读：<a href="/posts/go-gin-guide/">Gin 使用指南</a>。</p>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/go-gin-version-guide/</id>
    <link href="https://blogs.microcyan.com/posts/go-gin-version-guide/"/>
    <published>2026-03-12T00:48:00.000Z</published>
    <summary>对比 Gin 1.9、1.10、1.11 和 1.12 的适用场景，整理依赖管理、路由、中间件、参数绑定、测试和升级检查方法。</summary>
    <title>Gin 1.9 至 1.12 版本选择与升级指南</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="后端开发" scheme="https://blogs.microcyan.com/categories/backend/"/>
    <category term="后端开发" scheme="https://blogs.microcyan.com/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://blogs.microcyan.com/tags/Go/"/>
    <category term="Gin" scheme="https://blogs.microcyan.com/tags/Gin/"/>
    <content>
      <![CDATA[<!-- more --><p>这份文档整理 Gin 项目中最常见的写法，适合新建 API 服务或统一团队代码风格。</p><h2 id="基础服务"><a href="#基础服务" class="headerlink" title="基础服务"></a>基础服务</h2><figure class="highlight go"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"github.com/gin-gonic/gin"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line">    r := gin.Default()</span><br><span class="line"></span><br><span class="line">    r.GET(<span class="string">"/ping"</span>, <span class="function"><span class="keyword">func</span><span class="params">(c *gin.Context)</span></span> {</span><br><span class="line">        c.JSON(<span class="number">200</span>, gin.H{</span><br><span class="line">            <span class="string">"message"</span>: <span class="string">"pong"</span>,</span><br><span class="line">        })</span><br><span class="line">    })</span><br><span class="line"></span><br><span class="line">    r.Run(<span class="string">":8080"</span>)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="路由分组"><a href="#路由分组" class="headerlink" title="路由分组"></a>路由分组</h2><figure class="highlight go"><table><tbody><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">api := r.Group(<span class="string">"/api"</span>)</span><br><span class="line"></span><br><span class="line">api.GET(<span class="string">"/users"</span>, listUsers)</span><br><span class="line">api.POST(<span class="string">"/users"</span>, createUser)</span><br><span class="line">api.GET(<span class="string">"/users/:id"</span>, getUser)</span><br></pre></td></tr></tbody></table></figure><h2 id="参数绑定"><a href="#参数绑定" class="headerlink" title="参数绑定"></a>参数绑定</h2><figure class="highlight go"><table><tbody><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">type</span> CreateUserRequest <span class="keyword">struct</span> {</span><br><span class="line">  Name <span class="type">string</span> <span class="string">`json:"name" binding:"required"`</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">createUser</span><span class="params">(c *gin.Context)</span></span> {</span><br><span class="line">  <span class="keyword">var</span> req CreateUserRequest</span><br><span class="line">  <span class="keyword">if</span> err := c.ShouldBindJSON(&amp;req); err != <span class="literal">nil</span> {</span><br><span class="line">    c.JSON(<span class="number">400</span>, gin.H{<span class="string">"message"</span>: err.Error()})</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  }</span><br><span class="line"></span><br><span class="line">  c.JSON(<span class="number">200</span>, gin.H{<span class="string">"name"</span>: req.Name})</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>路径参数：</p><figure class="highlight go"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">id := c.Param(<span class="string">"id"</span>)</span><br></pre></td></tr></tbody></table></figure><p>查询参数：</p><figure class="highlight go"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">page := c.DefaultQuery(<span class="string">"page"</span>, <span class="string">"1"</span>)</span><br></pre></td></tr></tbody></table></figure><h2 id="中间件"><a href="#中间件" class="headerlink" title="中间件"></a>中间件</h2><figure class="highlight go"><table><tbody><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="function"><span class="keyword">func</span> <span class="title">AuthMiddleware</span><span class="params">()</span></span> gin.HandlerFunc {</span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(c *gin.Context)</span></span> {</span><br><span class="line">        token := c.GetHeader(<span class="string">"Authorization"</span>)</span><br><span class="line">        <span class="keyword">if</span> token == <span class="string">""</span> {</span><br><span class="line">            c.JSON(<span class="number">401</span>, gin.H{<span class="string">"message"</span>: <span class="string">"unauthorized"</span>})</span><br><span class="line">            c.Abort()</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        }</span><br><span class="line"></span><br><span class="line">        c.Next()</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="统一响应"><a href="#统一响应" class="headerlink" title="统一响应"></a>统一响应</h2><figure class="highlight go"><table><tbody><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">type</span> Response <span class="keyword">struct</span> {</span><br><span class="line">    Code    <span class="type">int</span>         <span class="string">`json:"code"`</span></span><br><span class="line">    Message <span class="type">string</span>      <span class="string">`json:"message"`</span></span><br><span class="line">    Data    <span class="keyword">interface</span>{} <span class="string">`json:"data,omitempty"`</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="项目结构建议"><a href="#项目结构建议" class="headerlink" title="项目结构建议"></a>项目结构建议</h2><figure class="highlight txt"><table><tbody><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">cmd/</span><br><span class="line">api/</span><br><span class="line">main.go</span><br><span class="line">internal/</span><br><span class="line">handler/</span><br><span class="line">middleware/</span><br><span class="line">service/</span><br><span class="line">repository/</span><br><span class="line">model/</span><br><span class="line">pkg/</span><br><span class="line">response/</span><br><span class="line">logger/</span><br></pre></td></tr></tbody></table></figure><p>简单项目可以少分层，但要保持路由、业务逻辑、数据库访问职责清楚。</p>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/go-gin-guide/</id>
    <link href="https://blogs.microcyan.com/posts/go-gin-guide/"/>
    <published>2026-03-05T08:38:00.000Z</published>
    <summary>Gin 路由、中间件、参数绑定、统一响应、错误处理和项目结构建议。</summary>
    <title>Gin 使用指南</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="后端开发" scheme="https://blogs.microcyan.com/categories/backend/"/>
    <category term="后端开发" scheme="https://blogs.microcyan.com/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://blogs.microcyan.com/tags/Go/"/>
    <content>
      <![CDATA[<!-- more --><h2 id="go-mod-下载依赖失败怎么办"><a href="#go-mod-下载依赖失败怎么办" class="headerlink" title="go mod 下载依赖失败怎么办"></a>go mod 下载依赖失败怎么办</h2><p>先查看环境：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">env</span> GOPROXY</span><br><span class="line">go <span class="built_in">env</span> GOSUMDB</span><br></pre></td></tr></tbody></table></figure><p>常用设置：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">env</span> -w GOPROXY=https://goproxy.cn,direct</span><br></pre></td></tr></tbody></table></figure><p>重新整理依赖：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go mod tidy</span><br></pre></td></tr></tbody></table></figure><h2 id="go-sum-冲突怎么办"><a href="#go-sum-冲突怎么办" class="headerlink" title="go.sum 冲突怎么办"></a>go.sum 冲突怎么办</h2><p><code>go.sum</code> 记录依赖校验信息，冲突时不要随便删整段。建议：</p><ol><li>先合并 <code>go.mod</code>。</li><li>运行 <code>go mod tidy</code>。</li><li>检查 <code>go.mod</code> 和 <code>go.sum</code> 的最终变化。</li><li>确认项目能正常测试和启动。</li></ol><h2 id="如何交叉编译"><a href="#如何交叉编译" class="headerlink" title="如何交叉编译"></a>如何交叉编译</h2><p>编译 Linux AMD64：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GOOS=linux GOARCH=amd64 go build -o app</span><br></pre></td></tr></tbody></table></figure><p>编译 macOS ARM64：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GOOS=darwin GOARCH=arm64 go build -o app</span><br></pre></td></tr></tbody></table></figure><p>编译 Windows：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GOOS=windows GOARCH=amd64 go build -o app.exe</span><br></pre></td></tr></tbody></table></figure><p>如果项目依赖 CGO，交叉编译会复杂很多，需要额外配置 C 编译工具链。</p><h2 id="panic-和-error-怎么区分"><a href="#panic-和-error-怎么区分" class="headerlink" title="panic 和 error 怎么区分"></a>panic 和 error 怎么区分</h2><p>普通业务错误应该返回 <code>error</code>，例如参数错误、数据库错误、外部接口失败。</p><p><code>panic</code> 适合不可恢复的程序错误，不应该用来做正常业务分支。</p><figure class="highlight go"><table><tbody><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">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">    <span class="keyword">return</span> fmt.Errorf(<span class="string">"create user: %w"</span>, err)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="goroutine-泄漏怎么排查"><a href="#goroutine-泄漏怎么排查" class="headerlink" title="goroutine 泄漏怎么排查"></a>goroutine 泄漏怎么排查</h2><p>常见原因：</p><ul><li>channel 没有关闭。</li><li>goroutine 一直阻塞等待。</li><li>context 没有取消。</li><li>定时器或 ticker 没有停止。</li><li>请求结束后后台任务还在跑。</li></ul><p>建议所有可能长期运行的任务都传入 <code>context.Context</code>：</p><figure class="highlight go"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(ctx context.Context)</span></span> {</span><br><span class="line">    <span class="keyword">for</span> {</span><br><span class="line">        <span class="keyword">select</span> {</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            <span class="comment">// do work</span></span><br><span class="line">        }</span><br><span class="line">    }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="map-并发读写报错怎么办"><a href="#map-并发读写报错怎么办" class="headerlink" title="map 并发读写报错怎么办"></a>map 并发读写报错怎么办</h2><p>普通 <code>map</code> 不是并发安全的。并发读写时可以使用：</p><ul><li><code>sync.RWMutex</code> 保护 map。</li><li><code>sync.Map</code>。</li><li>通过 channel 串行化写入。</li></ul><p>示例：</p><figure class="highlight go"><table><tbody><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">var</span> mu sync.RWMutex</span><br><span class="line">data := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>{}</span><br><span class="line"></span><br><span class="line">mu.Lock()</span><br><span class="line">data[<span class="string">"key"</span>] = <span class="string">"value"</span></span><br><span class="line">mu.Unlock()</span><br></pre></td></tr></tbody></table></figure><h2 id="部署-Go-服务要注意什么"><a href="#部署-Go-服务要注意什么" class="headerlink" title="部署 Go 服务要注意什么"></a>部署 Go 服务要注意什么</h2><p>建议检查：</p><ul><li>配置不要写死在代码里。</li><li>日志输出到文件或标准输出。</li><li>服务进程由 systemd、Docker 或进程管理工具托管。</li><li>健康检查接口可用。</li><li>监听地址和端口明确。</li><li>退出时能优雅关闭 HTTP 服务。</li></ul><h2 id="内存或-CPU-高怎么排查"><a href="#内存或-CPU-高怎么排查" class="headerlink" title="内存或 CPU 高怎么排查"></a>内存或 CPU 高怎么排查</h2><p>可以启用 pprof：</p><figure class="highlight go"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="string">"net/http/pprof"</span></span><br></pre></td></tr></tbody></table></figure><p>生产环境要加访问限制，不要直接暴露到公网。</p>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/go-faq/</id>
    <link href="https://blogs.microcyan.com/posts/go-faq/"/>
    <published>2026-03-02T03:18:00.000Z</published>
    <summary>Go 开发中 Go Module、环境变量、交叉编译、错误处理、并发、部署和性能相关常见问题。</summary>
    <title>Go 常见问题</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="Hexo" scheme="https://blogs.microcyan.com/tags/Hexo/"/>
    <category term="GitHub Pages" scheme="https://blogs.microcyan.com/tags/GitHub-Pages/"/>
    <category term="GitHub Actions" scheme="https://blogs.microcyan.com/tags/GitHub-Actions/"/>
    <category term="故障排查" scheme="https://blogs.microcyan.com/tags/%E6%95%85%E9%9A%9C%E6%8E%92%E6%9F%A5/"/>
    <category term="SEO" scheme="https://blogs.microcyan.com/tags/SEO/"/>
    <content>
      <![CDATA[<!-- more --><p>Hexo 部署到 GitHub Pages 后，首页能够打开并不代表站点配置完全正确。资源路径、文章路由、构建产物和 Actions 权限中的任何一项不一致，都可能导致页面样式丢失、链接 404 或更新不生效。</p><p>本站当前将源码与构建产物一同保存在私有仓库，由 Dell 定时拉取 <code>master</code> 分支中的 <code>public/</code>，再通过已有 frp 链路和 VPS Nginx 提供 HTTPS 访问；本文保留 GitHub Pages 场景的排查方法，供同类站点和历史部署参考。</p><p>排查时可以沿着“本地构建、构建产物、Pages 配置、线上访问路径”这条链路逐项确认。</p><h2 id="先确认站点属于哪种路径"><a href="#先确认站点属于哪种路径" class="headerlink" title="先确认站点属于哪种路径"></a>先确认站点属于哪种路径</h2><p>GitHub Pages 常见的访问方式有两种：</p><ul><li>用户或组织站点通常发布在 <code>https://&lt;用户名&gt;.github.io/</code>，根路径为 <code>/</code>。</li><li>项目站点通常发布在 <code>https://&lt;用户名&gt;.github.io/&lt;仓库名&gt;/</code>，根路径为 <code>/&lt;仓库名&gt;/</code>。</li></ul><p>如果仓库名为 <code>&lt;用户名&gt;.github.io</code>，它属于用户站点，会直接发布在域名根路径。Hexo 配置需要保持一致：</p><figure class="highlight yaml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">url:</span> <span class="string">https://&lt;用户名&gt;.github.io</span></span><br><span class="line"><span class="attr">root:</span> <span class="string">/</span></span><br><span class="line"><span class="attr">permalink:</span> <span class="string">posts/:name/</span></span><br></pre></td></tr></tbody></table></figure><p><code>url</code> 决定站点的完整地址，<code>root</code> 决定生成链接和静态资源时使用的根路径。两者配置不一致，通常会造成 CSS、JavaScript、图片或站内链接指向错误位置。</p><h2 id="页面能打开，但-CSS、JavaScript-或图片-404"><a href="#页面能打开，但-CSS、JavaScript-或图片-404" class="headerlink" title="页面能打开，但 CSS、JavaScript 或图片 404"></a>页面能打开，但 CSS、JavaScript 或图片 404</h2><p>常见原因包括：</p><ul><li>用户站点仍沿用了项目站点的 <code>root: /blog/</code> 配置。</li><li>Markdown 或主题配置中仍写有 <code>/blog/</code> 资源前缀。</li><li>线上仍在使用旧的构建产物或浏览器缓存。</li><li>图片文件没有提交，或者文件名大小写与引用不一致。</li></ul><p>本项目的站内静态资源直接从域名根路径引用，不再包含仓库名，例如：</p><figure class="highlight markdown"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">![<span class="string">示例图片</span>](<span class="link">/images/example.png</span>)</span><br></pre></td></tr></tbody></table></figure><p>修改配置后，先清理缓存并重新构建：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pnpm run clean</span><br><span class="line">pnpm run build</span><br></pre></td></tr></tbody></table></figure><p>然后检查 <code>public/</code> 中是否存在对应的 CSS、JavaScript 和图片文件。macOS 默认文件系统通常不区分文件名大小写，而 GitHub Pages 运行环境会区分，因此本地正常、线上 404 时尤其需要检查大小写。</p><h2 id="GitHub-Actions-成功，但网站没有更新"><a href="#GitHub-Actions-成功，但网站没有更新" class="headerlink" title="GitHub Actions 成功，但网站没有更新"></a>GitHub Actions 成功，但网站没有更新</h2><p>首先确认仓库 <code>Settings &gt; Pages</code> 中的发布源使用 GitHub Actions。随后检查以下内容：</p><ol><li>工作流监听的分支是否与实际推送分支一致。</li><li>最新提交是否触发了工作流，而不只是成功推送到其他分支。</li><li>构建任务和部署任务是否都完成，而不是仅构建成功。</li><li><code>github-pages</code> 环境中显示的部署提交是否为最新提交。</li><li>强制刷新页面或清除 CDN、浏览器缓存后是否恢复。</li></ol><p>例如，工作流只监听 <code>main</code> 和 <code>master</code> 时，推送到功能分支不会自动更新线上站点；需要将改动合并到发布分支，或者明确调整工作流触发规则。</p><h2 id="Actions-没有权限部署-Pages"><a href="#Actions-没有权限部署-Pages" class="headerlink" title="Actions 没有权限部署 Pages"></a>Actions 没有权限部署 Pages</h2><p>使用 GitHub 官方 Pages Actions 时，工作流通常需要以下权限：</p><figure class="highlight yaml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">  <span class="attr">pages:</span> <span class="string">write</span></span><br><span class="line">  <span class="attr">id-token:</span> <span class="string">write</span></span><br></pre></td></tr></tbody></table></figure><p>部署任务还应绑定 <code>github-pages</code> 环境。若日志中出现权限、OIDC 或 Pages 未启用等错误，需要同时检查工作流权限和仓库的 Pages 设置。</p><h2 id="本地构建成功，但-Actions-构建失败"><a href="#本地构建成功，但-Actions-构建失败" class="headerlink" title="本地构建成功，但 Actions 构建失败"></a>本地构建成功，但 Actions 构建失败</h2><p>这类问题通常来自本地与 CI 环境差异：</p><ul><li>Node.js 或包管理器版本不同。</li><li>锁文件没有提交，或者依赖未按锁文件安装。</li><li>文件名大小写不一致。</li><li>Markdown 引用了未提交或被 <code>.gitignore</code> 忽略的文件。</li><li>本地残留缓存掩盖了配置或依赖问题。</li></ul><p>提交前可以使用与 CI 接近的方式重新安装并构建：</p><figure class="highlight bash"><table><tbody><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">pnpm install --frozen-lockfile</span><br><span class="line">pnpm run clean</span><br><span class="line">pnpm run build</span><br></pre></td></tr></tbody></table></figure><p>如果 CI 固定了 Node.js 和 pnpm 版本，本地也应尽量使用相同版本复现问题。</p><h2 id="上传的构建目录不正确"><a href="#上传的构建目录不正确" class="headerlink" title="上传的构建目录不正确"></a>上传的构建目录不正确</h2><p>Hexo 默认将静态站点生成到 <code>public/</code>。Pages 工作流上传构建产物时，应确保路径指向该目录，而不是项目根目录或 Markdown 源文件目录。</p><figure class="highlight yaml"><table><tbody><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="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">artifact</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">actions/upload-pages-artifact@v3</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">public</span></span><br></pre></td></tr></tbody></table></figure><p>构建完成后至少应存在 <code>public/index.html</code>。如果首页文件不存在，应先检查 Hexo 构建命令和 <code>public_dir</code> 配置，而不是继续排查 Pages。</p><h2 id="首页正常，但文章刷新后-404"><a href="#首页正常，但文章刷新后-404" class="headerlink" title="首页正常，但文章刷新后 404"></a>首页正常，但文章刷新后 404</h2><p>Hexo 会为文章生成静态目录。使用 <code>posts/:name/</code> 规则时，一篇名为 <code>faq-deploy.md</code> 的文章通常对应：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">public/posts/faq-deploy/index.html</span><br></pre></td></tr></tbody></table></figure><p>可以依次确认：</p><ul><li>构建产物中是否存在该文件。</li><li>文章永久链接规则是否发生变化。</li><li>页面链接中的大小写和结尾斜杠是否正确。</li><li>旧链接是否需要设置重定向或继续保留原路径。</li></ul><p>如果构建目录中没有文章页面，问题在 Hexo 生成阶段；如果文件存在但线上 404，则继续检查上传目录和 Pages 部署结果。</p><h2 id="Sitemap-存在，但搜索引擎搜不到"><a href="#Sitemap-存在，但搜索引擎搜不到" class="headerlink" title="Sitemap 存在，但搜索引擎搜不到"></a>Sitemap 存在，但搜索引擎搜不到</h2><p>站点地图能够生成，不代表页面会立即被收录。当前博客的 Sitemap 地址为：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://blogs.microcyan.com/sitemap.xml</span><br></pre></td></tr></tbody></table></figure><p>需要确认 Sitemap 可以公开访问，页面没有 <code>noindex</code>，并且 <code>robots.txt</code> 没有禁止抓取。新站点或刚迁移的页面通常还需要等待搜索引擎重新抓取，也可以在站长平台主动提交 Sitemap。</p><h2 id="推荐排查顺序"><a href="#推荐排查顺序" class="headerlink" title="推荐排查顺序"></a>推荐排查顺序</h2><ol><li>本地执行清理和完整构建，确认没有报错。</li><li>检查 <code>public/index.html</code> 和目标文章页面是否生成。</li><li>检查 CSS、JavaScript、图片的生成路径。</li><li>核对 Hexo 的 <code>url</code>、<code>root</code> 和 <code>permalink</code>。</li><li>核对 Actions 的触发分支、权限和上传目录。</li><li>查看 Pages 部署环境对应的提交版本。</li><li>最后再排查浏览器缓存、CDN 缓存和搜索引擎收录延迟。</li></ol><h2 id="官方文档"><a href="#官方文档" class="headerlink" title="官方文档"></a>官方文档</h2><ul><li><a target="_blank" rel="noopener" href="https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages">使用自定义 GitHub Actions 工作流发布 Pages</a></li><li><a target="_blank" rel="noopener" href="https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site">配置 GitHub Pages 发布源</a></li><li><a target="_blank" rel="noopener" href="https://hexo.io/docs/configuration">Hexo 配置文档</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/faq-deploy/</id>
    <link href="https://blogs.microcyan.com/posts/faq-deploy/"/>
    <published>2025-08-22T08:19:00.000Z</published>
    <summary>整理 Hexo 博客部署到 GitHub Pages 时的根路径、资源 404、GitHub Actions 权限、构建产物、缓存和 Sitemap 收录问题。</summary>
    <title>Hexo 部署常见问题</title>
    <updated>2026-09-25T16:51:21.538Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="开发工具与效率" scheme="https://blogs.microcyan.com/categories/tools/"/>
    <category term="软件工程" scheme="https://blogs.microcyan.com/tags/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    <category term="开发规范" scheme="https://blogs.microcyan.com/tags/%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83/"/>
    <category term="团队协作" scheme="https://blogs.microcyan.com/tags/%E5%9B%A2%E9%98%9F%E5%8D%8F%E4%BD%9C/"/>
    <category term="发布管理" scheme="https://blogs.microcyan.com/tags/%E5%8F%91%E5%B8%83%E7%AE%A1%E7%90%86/"/>
    <content>
      <![CDATA[<!-- more --><p>团队规范的目标不是增加流程，而是减少重复沟通，让产品、前端、后端、测试和运维对交付边界有一致理解。具体项目可以在本文基础上补充目录结构、组件库、环境地址、负责人和审批要求。</p><h2 id="职责与交付边界"><a href="#职责与交付边界" class="headerlink" title="职责与交付边界"></a>职责与交付边界</h2><p>需求进入开发前，应明确以下内容：</p><ul><li>产品目标、核心流程和验收条件。</li><li>前后端接口、错误状态和空数据行为。</li><li>测试范围、回归范围和上线检查项。</li><li>数据变更、兼容窗口以及回滚方式。</li></ul><p>职责可以交叉，但每个关键结果都应有明确负责人，避免问题出现后才确认由谁处理。</p><h2 id="后端接口约定"><a href="#后端接口约定" class="headerlink" title="后端接口约定"></a>后端接口约定</h2><p>接口结构应保持稳定，避免同一个字段在不同状态下出现多种类型。</p><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">  <span class="attr">"code"</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"message"</span><span class="punctuation">:</span> <span class="string">"ok"</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"data"</span><span class="punctuation">:</span> <span class="punctuation">{</span><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><p>错误信息要同时服务用户与开发排查。面向用户的文案应清楚，堆栈、内部路径和敏感配置只进入日志或受控调试字段。</p><div class="md-table-scroll"><table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td><code>code</code></td><td>稳定的业务错误码</td></tr><tr><td><code>message</code></td><td>可读错误信息</td></tr><tr><td><code>data</code></td><td>成功数据，失败时保持约定类型</td></tr></tbody></table></div><p>分页接口应统一页码起点、每页数量和总数表达。例如约定 <code>page</code> 从 <code>1</code> 开始：</p><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">  <span class="attr">"page"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"pageSize"</span><span class="punctuation">:</span> <span class="number">20</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"total"</span><span class="punctuation">:</span> <span class="number">128</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"items"</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><p>字段名需要表达业务含义。同一概念不要同时出现 <code>userId</code>、<code>uid</code>、<code>memberId</code>，除非它们确实代表不同对象。</p><h2 id="前端开发约定"><a href="#前端开发约定" class="headerlink" title="前端开发约定"></a>前端开发约定</h2><p>组件应按照业务意图命名，并保持单一主要职责。组件难以阅读时，优先拆出明确的小组件或组合函数，而不是继续增加配置层级。</p><p>状态优先放在离使用位置最近的地方，只有跨页面共享或需要持久化时才提升为全局状态。</p><div class="md-table-scroll"><table><thead><tr><th>状态类型</th><th>推荐位置</th></tr></thead><tbody><tr><td>表单临时值</td><td>页面或表单组件内部</td></tr><tr><td>弹窗开关</td><td>触发弹窗的页面或局部组件</td></tr><tr><td>用户信息</td><td>全局状态或请求缓存</td></tr><tr><td>接口列表</td><td>请求缓存、页面状态或业务 store</td></tr></tbody></table></div><p>样式优先复用组件库和设计变量。列表渲染需要稳定标识时使用业务 <code>id</code>，只有列表不会重排、插入或删除时才考虑使用索引。</p><p>提交前至少检查：</p><ul><li>页面是否覆盖加载、空数据、错误和禁用状态。</li><li>文案是否能被真实用户理解。</li><li>移动端和桌面端是否都能正常阅读。</li><li>请求失败、登录失效和重复提交是否有处理。</li><li>新增逻辑是否可以用更直接的数据结构表达。</li></ul><h2 id="发布前"><a href="#发布前" class="headerlink" title="发布前"></a>发布前</h2><ul><li>确认需求范围、变更清单和验收结果。</li><li>确认环境变量和配置项已经准备。</li><li>确认数据库、缓存和第三方服务的兼容方案。</li><li>确认关键页面、接口、定时任务和数据指标的检查方式。</li><li>提前准备回滚版本、回滚命令和数据兼容方案。</li></ul><h2 id="发布中"><a href="#发布中" class="headerlink" title="发布中"></a>发布中</h2><p>发布过程应保留发布时间、版本号、提交记录、执行人和异常情况。涉及多个服务时，需要明确发布顺序和新旧版本兼容窗口。</p><p>数据库变更应优先采用向后兼容方式：先增加新结构并兼容读写，再切换业务，最后清理旧结构。不要让应用发布与不可逆数据修改形成单点风险。</p><h2 id="发布后"><a href="#发布后" class="headerlink" title="发布后"></a>发布后</h2><ul><li>检查登录、首页和核心业务流程。</li><li>检查接口错误率、日志、队列和告警。</li><li>检查静态资源、上传文件和第三方回调。</li><li>检查定时任务、缓存和数据同步状态。</li><li>记录异常、处理结果和需要继续观察的指标。</li></ul><h2 id="回滚"><a href="#回滚" class="headerlink" title="回滚"></a>回滚</h2><p>回滚方案应在发布前完成。至少明确：</p><ol><li>回滚到哪个应用版本。</li><li>哪些配置需要恢复。</li><li>数据结构是否向后兼容。</li><li>回滚后如何验证服务恢复。</li><li>哪些新数据需要补偿或重新处理。</li></ol><p>如果数据库变更不可逆，单纯回滚代码并不能恢复系统。此时应准备数据备份、补偿脚本或双写过渡方案。</p><h2 id="团队持续改进"><a href="#团队持续改进" class="headerlink" title="团队持续改进"></a>团队持续改进</h2><p>规范应从真实故障和协作成本中持续更新。每次严重问题复盘后，把可复用的检查项加入开发、测试或发布清单，而不是只记录一次性的处理过程。</p>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/engineering-collaboration-standards/</id>
    <link href="https://blogs.microcyan.com/posts/engineering-collaboration-standards/"/>
    <published>2025-08-20T06:19:00.000Z</published>
    <summary>整理研发团队中的前后端职责、接口契约、状态管理、代码检查、发布流程、上线验证和回滚约定。</summary>
    <title>研发团队协作规范：前后端约定、发布与回滚</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="服务器与运维" scheme="https://blogs.microcyan.com/categories/operations/"/>
    <category term="Docker" scheme="https://blogs.microcyan.com/tags/Docker/"/>
    <category term="服务器运维" scheme="https://blogs.microcyan.com/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%90%E7%BB%B4/"/>
    <category term="部署" scheme="https://blogs.microcyan.com/tags/%E9%83%A8%E7%BD%B2/"/>
    <category term="OpenList" scheme="https://blogs.microcyan.com/tags/OpenList/"/>
    <content>
      <![CDATA[<!-- more --><p>OpenList 和 AList 都常用于把本地目录、网盘、对象存储等资源整理成一个 Web 文件列表。适合个人文件管理、素材预览、临时分享和 WebDAV 场景。</p><blockquote><p><strong>官方入口</strong></p><ul><li><a target="_blank" rel="noopener" href="https://openlistteam.github.io/OpenList-Docs/">OpenList 文档</a></li><li><a target="_blank" rel="noopener" href="https://github.com/OpenListTeam/OpenList">OpenList GitHub</a></li><li><a target="_blank" rel="noopener" href="https://alistgo.com/">AList 文档</a></li></ul></blockquote><h2 id="选型说明"><a href="#选型说明" class="headerlink" title="选型说明"></a>选型说明</h2><div class="md-table-scroll"><table><thead><tr><th>项目</th><th>说明</th></tr></thead><tbody><tr><td>AList</td><td>较早被大量使用的文件列表程序，生态资料比较多</td></tr><tr><td>OpenList</td><td>AList 的社区 fork，强调长期治理、透明维护和开源延续</td></tr></tbody></table></div><p>如果是新部署，可以优先看 OpenList；如果已有 AList，不建议没有备份就直接迁移。</p><h2 id="OpenList-Docker-部署"><a href="#OpenList-Docker-部署" class="headerlink" title="OpenList Docker 部署"></a>OpenList Docker 部署</h2><p>准备数据目录：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p /etc/openlist</span><br></pre></td></tr></tbody></table></figure><p>使用当前用户运行：</p><figure class="highlight bash"><table><tbody><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">docker run --user $(<span class="built_in">id</span> -u):$(<span class="built_in">id</span> -g) -d \</span><br><span class="line">  --restart=unless-stopped \</span><br><span class="line">  -v /etc/openlist:/opt/openlist/data \</span><br><span class="line">  -p 5244:5244 \</span><br><span class="line">  -e UMASK=022 \</span><br><span class="line">  --name openlist \</span><br><span class="line">  openlistteam/openlist:latest</span><br></pre></td></tr></tbody></table></figure><p>如果使用容器内默认用户，要注意目录权限：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">chown</span> -R 1001:1001 /etc/openlist</span><br></pre></td></tr></tbody></table></figure><p>查看首次管理员密码：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker logs openlist</span><br></pre></td></tr></tbody></table></figure><p>重置管理员密码：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> -it openlist ./openlist admin random</span><br><span class="line">docker <span class="built_in">exec</span> -it openlist ./openlist admin <span class="built_in">set</span> NEW_PASSWORD</span><br></pre></td></tr></tbody></table></figure><h2 id="AList-Docker-部署"><a href="#AList-Docker-部署" class="headerlink" title="AList Docker 部署"></a>AList Docker 部署</h2><figure class="highlight bash"><table><tbody><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">docker run -d \</span><br><span class="line">  --restart=unless-stopped \</span><br><span class="line">  -v /etc/alist:/opt/alist/data \</span><br><span class="line">  -p 5244:5244 \</span><br><span class="line">  -e PUID=0 \</span><br><span class="line">  -e PGID=0 \</span><br><span class="line">  -e UMASK=022 \</span><br><span class="line">  --name alist \</span><br><span class="line">  xhofe/alist:latest</span><br></pre></td></tr></tbody></table></figure><p>查看日志：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker logs alist</span><br></pre></td></tr></tbody></table></figure><h2 id="Docker-Compose-示例"><a href="#Docker-Compose-示例" class="headerlink" title="Docker Compose 示例"></a>Docker Compose 示例</h2><figure class="highlight yaml"><table><tbody><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="attr">services:</span></span><br><span class="line">  <span class="attr">openlist:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">openlistteam/openlist:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">openlist</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">"5244:5244"</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/etc/openlist:/opt/openlist/data</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">UMASK=022</span></span><br></pre></td></tr></tbody></table></figure><p>启动：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d</span><br></pre></td></tr></tbody></table></figure><h2 id="Nginx-反向代理"><a href="#Nginx-反向代理" class="headerlink" title="Nginx 反向代理"></a>Nginx 反向代理</h2><figure class="highlight nginx"><table><tbody><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="section">server</span> {</span><br><span class="line">  <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">  <span class="attribute">server_name</span> files.example.com;</span><br><span class="line"></span><br><span class="line">  <span class="section">location</span> / {</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:5244;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>如果有大文件上传，还要增加：</p><figure class="highlight nginx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">client_max_body_size</span> <span class="number">1024m</span>;</span><br><span class="line"><span class="attribute">proxy_request_buffering</span> <span class="literal">off</span>;</span><br></pre></td></tr></tbody></table></figure><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="访问不了-5244"><a href="#访问不了-5244" class="headerlink" title="访问不了 5244"></a>访问不了 5244</h3><p>检查容器和端口：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker ps</span><br><span class="line">ss -lntp | grep 5244</span><br></pre></td></tr></tbody></table></figure><p>如果服务器有防火墙：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">firewall-cmd --add-port=5244/tcp --permanent</span><br><span class="line">firewall-cmd --reload</span><br></pre></td></tr></tbody></table></figure><p>生产环境更建议只开放 80/443，通过 Nginx 反代到 <code>127.0.0.1:5244</code>。</p><h3 id="上传失败或目录无权限"><a href="#上传失败或目录无权限" class="headerlink" title="上传失败或目录无权限"></a>上传失败或目录无权限</h3><p>检查挂载目录权限：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> -ld /etc/openlist</span><br></pre></td></tr></tbody></table></figure><p>如果容器使用 <code>1001</code> 用户运行：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">chown</span> -R 1001:1001 /etc/openlist</span><br></pre></td></tr></tbody></table></figure><h3 id="反向代理后下载链接不对"><a href="#反向代理后下载链接不对" class="headerlink" title="反向代理后下载链接不对"></a>反向代理后下载链接不对</h3><p>检查代理头：</p><figure class="highlight nginx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line"><span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br></pre></td></tr></tbody></table></figure><p>如果站点放在子目录，还要确认程序本身是否支持配置站点 URL 或路径前缀。</p><h3 id="WebDAV-连不上"><a href="#WebDAV-连不上" class="headerlink" title="WebDAV 连不上"></a>WebDAV 连不上</h3><p>优先确认：</p><ul><li>用户是否开启 WebDAV 权限。</li><li>客户端地址是否填写完整。</li><li>反向代理是否正确转发 <code>PROPFIND</code>、<code>PUT</code>、<code>DELETE</code> 等方法。</li><li>HTTPS 证书是否可信。</li></ul><h2 id="安全建议"><a href="#安全建议" class="headerlink" title="安全建议"></a>安全建议</h2><ul><li>不要把管理后台暴露给完全公开的网络，至少设置强密码。</li><li>反向代理层可以加 Basic Auth、IP 白名单或 Cloudflare Access。</li><li>不要把服务器根目录挂载给文件列表程序。</li><li>分享外链要设置过期时间或访问密码。</li><li>定期备份配置目录。</li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/ops-openlist-alist/</id>
    <link href="https://blogs.microcyan.com/posts/ops-openlist-alist/"/>
    <published>2025-08-14T08:25:00.000Z</published>
    <summary>OpenList、AList 文件列表程序的 Docker 部署、端口、管理员密码、反向代理、权限、WebDAV 和常见问题整理。</summary>
    <title>OpenList 与 AList 部署常见问题</title>
    <updated>2026-09-08T10:34:43.298Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="数据库与存储" scheme="https://blogs.microcyan.com/categories/data-storage/"/>
    <category term="MinIO" scheme="https://blogs.microcyan.com/tags/MinIO/"/>
    <category term="服务器运维" scheme="https://blogs.microcyan.com/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%90%E7%BB%B4/"/>
    <category term="部署" scheme="https://blogs.microcyan.com/tags/%E9%83%A8%E7%BD%B2/"/>
    <category term="Rclone" scheme="https://blogs.microcyan.com/tags/Rclone/"/>
    <content>
      <![CDATA[<!-- more --><p>本文用于在 MinIO、阿里云 OSS 等对象存储之间同步数据，适合做对象存储迁移、备份和数据整理。</p><h2 id="安装-rclone"><a href="#安装-rclone" class="headerlink" title="安装 rclone"></a>安装 rclone</h2><p>官方安装文档：</p><figure class="highlight txt"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://rclone.org/install/</span><br></pre></td></tr></tbody></table></figure><p>Linux 安装脚本：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> -v</span><br><span class="line">curl https://rclone.org/install.sh | <span class="built_in">sudo</span> bash</span><br></pre></td></tr></tbody></table></figure><h2 id="配置文件位置"><a href="#配置文件位置" class="headerlink" title="配置文件位置"></a>配置文件位置</h2><p>rclone 默认配置文件通常在：</p><figure class="highlight txt"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">~/.config/rclone/rclone.conf</span><br></pre></td></tr></tbody></table></figure><p>可通过命令交互式配置：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rclone config</span><br></pre></td></tr></tbody></table></figure><h2 id="S3-兼容配置示例"><a href="#S3-兼容配置示例" class="headerlink" title="S3 兼容配置示例"></a>S3 兼容配置示例</h2><p>MinIO 示例：</p><figure class="highlight ini"><table><tbody><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="section">[minio]</span></span><br><span class="line"><span class="attr">type</span> = s3</span><br><span class="line"><span class="attr">provider</span> = Minio</span><br><span class="line"><span class="attr">access_key_id</span> = your-access-key</span><br><span class="line"><span class="attr">secret_access_key</span> = your-secret-key</span><br><span class="line"><span class="attr">endpoint</span> = https://minio.example.com</span><br></pre></td></tr></tbody></table></figure><p>阿里云 OSS 示例：</p><figure class="highlight ini"><table><tbody><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="section">[aliyun-oss]</span></span><br><span class="line"><span class="attr">type</span> = s3</span><br><span class="line"><span class="attr">provider</span> = Alibaba</span><br><span class="line"><span class="attr">access_key_id</span> = your-access-key</span><br><span class="line"><span class="attr">secret_access_key</span> = your-secret-key</span><br><span class="line"><span class="attr">endpoint</span> = oss-cn-hangzhou.aliyuncs.com</span><br><span class="line"><span class="attr">acl</span> = private</span><br></pre></td></tr></tbody></table></figure><p>请不要把真实密钥提交到 Git 仓库。迁移完成后也要检查服务器上的配置文件权限。</p><h2 id="同步命令"><a href="#同步命令" class="headerlink" title="同步命令"></a>同步命令</h2><p>从 MinIO 同步到阿里云 OSS：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rclone <span class="built_in">sync</span> minio:bucket aliyun-oss:bucket</span><br></pre></td></tr></tbody></table></figure><p>常用参数：</p><div class="md-table-scroll"><table><thead><tr><th>参数</th><th>作用</th></tr></thead><tbody><tr><td><code>--dry-run</code></td><td>只预演，不真正执行</td></tr><tr><td><code>--progress</code></td><td>显示传输进度</td></tr><tr><td><code>--transfers 8</code></td><td>并发传输数量</td></tr><tr><td><code>--checkers 16</code></td><td>并发检查数量</td></tr><tr><td><code>--log-file rclone.log</code></td><td>输出日志文件</td></tr></tbody></table></div><p>建议先预演：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rclone <span class="built_in">sync</span> minio:bucket aliyun-oss:bucket --dry-run</span><br></pre></td></tr></tbody></table></figure><p>确认无误后再正式执行：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rclone <span class="built_in">sync</span> minio:bucket aliyun-oss:bucket --progress --log-file rclone.log</span><br></pre></td></tr></tbody></table></figure><h2 id="sync-和-copy-的区别"><a href="#sync-和-copy-的区别" class="headerlink" title="sync 和 copy 的区别"></a>sync 和 copy 的区别</h2><div class="md-table-scroll"><table><thead><tr><th>命令</th><th>行为</th></tr></thead><tbody><tr><td><code>rclone copy</code></td><td>只复制新增或变化的文件，不删除目标端多余文件</td></tr><tr><td><code>rclone sync</code></td><td>让目标端和源端保持一致，会删除目标端多余文件</td></tr></tbody></table></div><p>如果不确定目标端是否可以删除文件，优先使用 <code>copy</code> 或 <code>sync --dry-run</code>。</p>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/ops-rclone/</id>
    <link href="https://blogs.microcyan.com/posts/ops-rclone/"/>
    <published>2025-08-07T09:26:00.000Z</published>
    <summary>使用 rclone 在 MinIO、阿里云 OSS 等 S3 兼容对象存储之间同步和迁移数据。</summary>
    <title>rclone 对象存储迁移</title>
    <updated>2026-09-08T10:34:43.297Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="数据库与存储" scheme="https://blogs.microcyan.com/categories/data-storage/"/>
    <category term="MinIO" scheme="https://blogs.microcyan.com/tags/MinIO/"/>
    <category term="Nginx" scheme="https://blogs.microcyan.com/tags/Nginx/"/>
    <category term="Docker" scheme="https://blogs.microcyan.com/tags/Docker/"/>
    <category term="服务器运维" scheme="https://blogs.microcyan.com/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%BF%90%E7%BB%B4/"/>
    <category term="部署" scheme="https://blogs.microcyan.com/tags/%E9%83%A8%E7%BD%B2/"/>
    <content>
      <![CDATA[<!-- more --><p>本文整理 MinIO 对象存储的 Docker 安装、数据目录挂载、控制台端口、Nginx 反向代理和完整安装脚本，适合快速搭建自有对象存储服务。</p><h2 id="快捷安装"><a href="#快捷安装" class="headerlink" title="快捷安装"></a>快捷安装</h2><p>安装 Docker 并启动：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun</span><br><span class="line"><span class="built_in">sudo</span> systemctl start docker</span><br></pre></td></tr></tbody></table></figure><p>拉取 MinIO 镜像：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull minio/minio</span><br></pre></td></tr></tbody></table></figure><p>如果需要同时安装 Nginx，也可以使用 LNMP 安装包：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget https://soft.lnmp.com/lnmp/lnmp2.0.tar.gz -O lnmp2.0.tar.gz &amp;&amp; tar zxf lnmp2.0.tar.gz &amp;&amp; <span class="built_in">cd</span> lnmp2.0 &amp;&amp; ./install.sh nginx</span><br></pre></td></tr></tbody></table></figure><p>启动 MinIO：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -p 9000:9000 -p 9001:9001 --name minio -e <span class="string">"MINIO_ROOT_USER=XXX"</span> -e <span class="string">"MINIO_ROOT_PASSWORD=XXXX"</span> -v /home/wwwroot/minio:/data minio/minio server /data --console-address <span class="string">":9001"</span></span><br></pre></td></tr></tbody></table></figure><p>说明：</p><div class="md-table-scroll"><table><thead><tr><th>配置</th><th>作用</th></tr></thead><tbody><tr><td><code>9000</code></td><td>S3 API 访问端口</td></tr><tr><td><code>9001</code></td><td>Web 控制台端口</td></tr><tr><td><code>/home/wwwroot/minio:/data</code></td><td>把宿主机目录挂载为对象存储数据目录</td></tr><tr><td><code>MINIO_ROOT_USER</code></td><td>管理员用户名</td></tr><tr><td><code>MINIO_ROOT_PASSWORD</code></td><td>管理员密码</td></tr></tbody></table></div><h2 id="完整安装脚本"><a href="#完整安装脚本" class="headerlink" title="完整安装脚本"></a>完整安装脚本</h2><p>下面脚本会安装 Docker、启动 Docker、拉取 MinIO 和 Nginx 镜像、创建数据目录、读取管理员用户名和密码、启动 MinIO 容器并输出容器内网 IP。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装 Docker</span></span><br><span class="line">curl -sSL https://get.daocloud.io/docker | sh</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动 Docker</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl start docker</span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装 MinIO</span></span><br><span class="line">docker pull minio/minio</span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装 Nginx</span></span><br><span class="line">docker pull nginx:latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建所需要的文件夹</span></span><br><span class="line">minio_name=<span class="string">"/root/minio/data"</span></span><br><span class="line">minio_certs=<span class="string">"/root/minio/certs"</span></span><br><span class="line">minio_nginx=<span class="string">"/root/minio/nginx"</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">mkdir</span> -p <span class="string">"<span class="variable">$minio_name</span>"</span></span><br><span class="line"><span class="built_in">mkdir</span> -p <span class="string">"<span class="variable">$minio_certs</span>"</span></span><br><span class="line"><span class="built_in">mkdir</span> -p <span class="string">"<span class="variable">$minio_nginx</span>"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 定义登录用户名</span></span><br><span class="line">username=<span class="string">""</span></span><br><span class="line"><span class="built_in">read</span> -p <span class="string">"请输入minio管理用户名:"</span> username</span><br><span class="line"><span class="keyword">if</span> [ -z <span class="string">"<span class="variable">$username</span>"</span> ]; <span class="keyword">then</span></span><br><span class="line">  username=<span class="string">"minioadmin"</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 定义登录密码</span></span><br><span class="line">password=<span class="string">""</span></span><br><span class="line"><span class="built_in">read</span> -p <span class="string">"请输入minio管理密码:"</span> password</span><br><span class="line"><span class="keyword">if</span> [ -z <span class="string">"<span class="variable">$password</span>"</span> ]; <span class="keyword">then</span></span><br><span class="line">  password=<span class="string">"minioadmin123"</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动 MinIO 容器</span></span><br><span class="line">docker run -d \</span><br><span class="line">-e MINIO_ACCESS_KEY=<span class="string">"<span class="variable">$username</span>"</span> \</span><br><span class="line">-e MINIO_SECRET_KEY=<span class="string">"<span class="variable">$password</span>"</span> \</span><br><span class="line">--name minio \</span><br><span class="line">-v <span class="string">"<span class="variable">$minio_name</span>"</span>:/data \</span><br><span class="line">-v <span class="string">"<span class="variable">$minio_certs</span>"</span>:/root/.minio/certs/CAs \</span><br><span class="line">minio/minio server /data</span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取容器 IP</span></span><br><span class="line">IPAddress=$(docker inspect -f <span class="string">'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'</span> minio)</span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">"-------------------------------------"</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">"minio管理用户名：<span class="variable">${username}</span>"</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">"minio管理密码：<span class="variable">${password}</span>"</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">"minio内网IP：<span class="variable">${IPAddress}</span>"</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">"-------------------------------------"</span></span><br></pre></td></tr></tbody></table></figure><p>新版本 MinIO 更推荐使用 <code>MINIO_ROOT_USER</code> 和 <code>MINIO_ROOT_PASSWORD</code>。如果使用新版镜像，可以把脚本里的 <code>MINIO_ACCESS_KEY</code> 和 <code>MINIO_SECRET_KEY</code> 改成下面这种写法：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-e MINIO_ROOT_USER=<span class="string">"<span class="variable">$username</span>"</span> \</span><br><span class="line">-e MINIO_ROOT_PASSWORD=<span class="string">"<span class="variable">$password</span>"</span> \</span><br></pre></td></tr></tbody></table></figure><h2 id="查看容器状态"><a href="#查看容器状态" class="headerlink" title="查看容器状态"></a>查看容器状态</h2><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker ps</span><br><span class="line">docker logs minio</span><br></pre></td></tr></tbody></table></figure><p>如果需要重启：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker restart minio</span><br></pre></td></tr></tbody></table></figure><h2 id="Nginx-反向代理"><a href="#Nginx-反向代理" class="headerlink" title="Nginx 反向代理"></a>Nginx 反向代理</h2><p>启动 Nginx 容器：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name nginx -p 80:80 -p 443:443 -v /root/minio/nginx:/etc/nginx/conf.d -d nginx</span><br></pre></td></tr></tbody></table></figure><p>Nginx 配置示例：</p><figure class="highlight nginx"><table><tbody><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="section">server</span> {</span><br><span class="line">  <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">  <span class="attribute">listen</span> <span class="number">443</span> ssl;</span><br><span class="line">  <span class="attribute">server_name</span> server;</span><br><span class="line">  <span class="attribute">ssl_certificate</span> /etc/nginx/conf.d/ssl/server/public.pem;</span><br><span class="line">  <span class="attribute">ssl_certificate_key</span> /etc/nginx/conf.d/ssl/server/public.key;</span><br><span class="line">  <span class="attribute">ssl_session_timeout</span> <span class="number">5m</span>;</span><br><span class="line">  <span class="attribute">ssl_ciphers</span> ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;</span><br><span class="line">  <span class="attribute">ssl_protocols</span> TLSv1 TLSv1.<span class="number">1</span> TLSv1.<span class="number">2</span>;</span><br><span class="line">  <span class="attribute">ssl_prefer_server_ciphers</span> <span class="literal">on</span>;</span><br><span class="line"></span><br><span class="line">  <span class="section">location</span> / {</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$http_host</span>;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://172.17.0.2:9000;</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>反向代理时需要注意：</p><ul><li>API 端口和控制台端口最好使用不同域名或不同路径。</li><li>需要保留 Host、真实 IP 和协议头。</li><li>如果启用 HTTPS，要确认 MinIO 控制台外部地址配置正确。</li></ul><p>更通用的反向代理结构：</p><figure class="highlight nginx"><table><tbody><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="section">server</span> {</span><br><span class="line">  <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">  <span class="attribute">server_name</span> oss.example.com;</span><br><span class="line"></span><br><span class="line">  <span class="section">location</span> / {</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:9000;</span><br><span class="line">  }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="指定存储目录"><a href="#指定存储目录" class="headerlink" title="指定存储目录"></a>指定存储目录</h2><p>如果需要指定数据目录，可以挂载到 <code>/usr/local/minio/data</code>：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -d -p 9000:9000 -p 9001:9001 -e MINIO_ACCESS_KEY=LinXi -e MINIO_SECRET_KEY=LinXi123 --name minio -v /usr/local/minio/data:/data minio/minio server /data --console-address <span class="string">":9001"</span></span><br></pre></td></tr></tbody></table></figure><p>同样建议在新版 MinIO 中改用：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -d -p 9000:9000 -p 9001:9001 -e MINIO_ROOT_USER=LinXi -e MINIO_ROOT_PASSWORD=LinXi123 --name minio -v /usr/local/minio/data:/data minio/minio server /data --console-address <span class="string">":9001"</span></span><br></pre></td></tr></tbody></table></figure><h2 id="生产环境建议"><a href="#生产环境建议" class="headerlink" title="生产环境建议"></a>生产环境建议</h2><ul><li>不要使用过短或弱口令。</li><li>数据目录要有备份策略。</li><li>控制台端口不要直接暴露给所有公网来源。</li><li>大文件和多用户场景要评估磁盘、带宽和备份成本。</li><li>如果只是项目静态资源，也可以评估云厂商 OSS、COS、S3 等托管服务。</li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/ops-minio/</id>
    <link href="https://blogs.microcyan.com/posts/ops-minio/"/>
    <published>2025-08-04T15:15:00.000Z</published>
    <summary>使用 Docker 安装 MinIO，对象存储数据目录、控制台端口、账号密码和 Nginx 反向代理配置说明。</summary>
    <title>MinIO 对象存储安装</title>
    <updated>2026-09-08T10:34:43.297Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="人工智能" scheme="https://blogs.microcyan.com/categories/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/"/>
    <category term="物联网" scheme="https://blogs.microcyan.com/tags/%E7%89%A9%E8%81%94%E7%BD%91/"/>
    <category term="Agent" scheme="https://blogs.microcyan.com/tags/Agent/"/>
    <category term="RAG" scheme="https://blogs.microcyan.com/tags/RAG/"/>
    <category term="AIoT" scheme="https://blogs.microcyan.com/tags/AIoT/"/>
    <category term="异常检测" scheme="https://blogs.microcyan.com/tags/%E5%BC%82%E5%B8%B8%E6%A3%80%E6%B5%8B/"/>
    <content>
      <![CDATA[<!-- more --><p>AIoT 不是给设备接一个聊天模型。它是把设备采集、边缘计算、云端数据平台和 AI 能力组合起来，让系统能够识别异常、预测趋势、辅助运维或生成控制建议。</p><h2 id="一条完整链路"><a href="#一条完整链路" class="headerlink" title="一条完整链路"></a>一条完整链路</h2><figure class="highlight text"><table><tbody><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">传感器/PLC</span><br><span class="line">  -&gt; 边缘网关清洗和本地规则</span><br><span class="line">  -&gt; MQTT Broker</span><br><span class="line">  -&gt; Kafka/流处理</span><br><span class="line">  -&gt; 时序数据库 + 数据湖</span><br><span class="line">  -&gt; 特征处理</span><br><span class="line">  -&gt; 规则/统计/机器学习模型</span><br><span class="line">  -&gt; 告警、工单、看板和运维助手</span><br></pre></td></tr></tbody></table></figure><p>边缘规则负责必须立即执行的安全联锁，云端模型适合跨设备、长时间窗口和复杂模式分析。不能把停机保护完全依赖网络和大模型。</p><h2 id="AI-能力分三类"><a href="#AI-能力分三类" class="headerlink" title="AI 能力分三类"></a>AI 能力分三类</h2><h3 id="规则与统计"><a href="#规则与统计" class="headerlink" title="规则与统计"></a>规则与统计</h3><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">温度 &gt; 80 持续 30 秒 -&gt; 告警</span><br><span class="line">过去 10 分钟震动均值偏离基线 3 倍 -&gt; 异常</span><br></pre></td></tr></tbody></table></figure><p>它们可解释、上线快，很多项目应先从这里开始。</p><h3 id="预测与识别模型"><a href="#预测与识别模型" class="headerlink" title="预测与识别模型"></a>预测与识别模型</h3><p>可用于异常检测、剩余寿命预测、能耗预测、图像质检和声音故障识别。模型必须通过离线回放和在线灰度评估误报、漏报、漂移与推理成本。</p><h3 id="RAG-与-Agent"><a href="#RAG-与-Agent" class="headerlink" title="RAG 与 Agent"></a>RAG 与 Agent</h3><p>RAG 运维助手可以检索设备手册、故障码、维修记录和历史工单，并给出带来源的排查建议。Agent 可以调用只读工具查询实时状态：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">用户询问故障</span><br><span class="line">  -&gt; 检索手册和工单</span><br><span class="line">  -&gt; 查询设备最近指标与告警</span><br><span class="line">  -&gt; 生成诊断建议和证据</span><br></pre></td></tr></tbody></table></figure><p>控制设备的工具必须做参数校验、权限、审批、幂等、超时和审计。默认先只读，危险写操作由人确认，模型文本不能绕过业务安全规则。</p><h2 id="数据准备比模型更重要"><a href="#数据准备比模型更重要" class="headerlink" title="数据准备比模型更重要"></a>数据准备比模型更重要</h2><p>至少保证：</p><ul><li>设备 ID、测点、单位和时间语义统一。</li><li>同时保留设备时间与服务端接收时间。</li><li>缺失、重复、乱序和异常值有明确处理。</li><li>维修记录能关联故障、部件和设备型号。</li><li>训练、评测与线上数据遵守权限和隐私边界。</li></ul><h2 id="一个渐进式实施路线"><a href="#一个渐进式实施路线" class="headerlink" title="一个渐进式实施路线"></a>一个渐进式实施路线</h2><ol><li>建立可靠采集、时序存储和告警闭环。</li><li>用规则和统计基线验证数据质量。</li><li>对单个高价值场景训练或接入模型。</li><li>建立离线评测、灰度、监控和回滚。</li><li>用 RAG 整合手册与工单。</li><li>最后再开放受控 Agent 工具。</li></ol><p>这种顺序能让每一步都产生业务价值，也避免在数据基础不稳时先做一个看似聪明但无法负责的 AI 控制层。</p><h2 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h2><ul><li><a target="_blank" rel="noopener" href="https://www.nist.gov/itl/ai-risk-management-framework">NIST AI Risk Management Framework</a></li><li><a target="_blank" rel="noopener" href="https://opentelemetry.io/docs/">OpenTelemetry Documentation</a></li><li><a target="_blank" rel="noopener" href="https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html">OASIS：MQTT 5.0</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/iot-aiot-architecture/</id>
    <link href="https://blogs.microcyan.com/posts/iot-aiot-architecture/"/>
    <published>2025-07-27T07:37:00.000Z</published>
    <summary>以设备云为骨架，说明规则引擎、时序特征、预测模型、RAG 运维助手和 Agent 工具调用如何安全组合。</summary>
    <title>AIoT 系统怎么搭：从设备数据到异常检测与运维助手</title>
    <updated>2026-09-08T13:56:02.683Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="人工智能" scheme="https://blogs.microcyan.com/categories/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/"/>
    <category term="AI 编程" scheme="https://blogs.microcyan.com/tags/AI-%E7%BC%96%E7%A8%8B/"/>
    <category term="Agent" scheme="https://blogs.microcyan.com/tags/Agent/"/>
    <category term="Codex" scheme="https://blogs.microcyan.com/tags/Codex/"/>
    <category term="Skill" scheme="https://blogs.microcyan.com/tags/Skill/"/>
    <category term="AGENTS.md" scheme="https://blogs.microcyan.com/tags/AGENTS-md/"/>
    <content>
      <![CDATA[<!-- more --><p>AI 编程已经不只是生成一个函数。编程 Agent 可以读取仓库、搜索代码、修改文件、运行命令、检查测试，并根据结果继续迭代。</p><p>模型只是其中的推理核心。真正让它能在项目里完成工作的是一整套上下文、工具、规则、执行循环和安全控制。</p><h2 id="从一个-Bug-看完整过程"><a href="#从一个-Bug-看完整过程" class="headerlink" title="从一个 Bug 看完整过程"></a>从一个 Bug 看完整过程</h2><p>用户提出：</p><blockquote><p>修复登录按钮点击没有反应的问题，并补充测试。</p></blockquote><p>一个完整流程可能是：</p><figure class="highlight text"><table><tbody><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><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">→ 查看 Diff</span><br><span class="line">→ 报告原因、改动和验证结果</span><br></pre></td></tr></tbody></table></figure><p>如果测试失败，Agent 会读取错误信息并决定下一步。这个“观察、行动、再观察”的循环，是它与一次性代码生成的重要区别。</p><h2 id="核心概念对照"><a href="#核心概念对照" class="headerlink" title="核心概念对照"></a>核心概念对照</h2><div class="md-table-scroll"><table><thead><tr><th>概念</th><th>解决的问题</th><th>在 Bug 场景中的作用</th></tr></thead><tbody><tr><td>模型</td><td>如何理解和生成</td><td>分析报错、推理原因、生成修改</td></tr><tr><td>Agent</td><td>谁组织整个任务</td><td>决定查什么、改什么、何时验证</td></tr><tr><td>Context</td><td>当前能看到什么</td><td>用户要求、代码、规则、测试输出</td></tr><tr><td>Tool</td><td>怎样执行动作</td><td>读写文件、Shell、Git、浏览器</td></tr><tr><td>Skill</td><td>某类任务怎样做</td><td>规定复现、修复、回归的工作流</td></tr><tr><td><code>AGENTS.md</code></td><td>在当前仓库遵守什么</td><td>包管理器、修改范围、测试命令</td></tr><tr><td>MCP</td><td>怎样连接外部系统</td><td>访问 GitHub、设计稿或数据库</td></tr><tr><td>RAG</td><td>怎样找到外部知识</td><td>检索规范、历史故障或内部文档</td></tr><tr><td>Memory</td><td>哪些信息跨任务保留</td><td>用户偏好、长期项目背景</td></tr><tr><td>Harness</td><td>怎样运行并约束 Agent</td><td>组装上下文、执行工具、权限和停止条件</td></tr></tbody></table></div><h2 id="AGENTS-md-与-Skill"><a href="#AGENTS-md-与-Skill" class="headerlink" title="AGENTS.md 与 Skill"></a><code>AGENTS.md</code> 与 Skill</h2><p><code>AGENTS.md</code> 是仓库或目录级的项目指令文件。它适合记录：</p><ul><li>项目结构和技术栈。</li><li>统一包管理器和常用命令。</li><li>编码规范与模块边界。</li><li>哪些文件不能修改。</li><li>完成任务后的验收要求。</li></ul><p>OpenAI 的 <a target="_blank" rel="noopener" href="https://learn.chatgpt.com/docs/agent-configuration/agents-md">AGENTS.md 文档</a>说明，Codex 会按目录层级发现项目指导，并让更具体位置的规则覆盖上层通用规则。</p><p>Skill 是可复用的专项工作流。例如“制作 PDF”“查询官方文档”“执行数据库迁移检查”。在 Codex 中，Skill 是一个包含必需 <code>SKILL.md</code> 以及可选脚本、参考资料和资源的目录，详见 <a target="_blank" rel="noopener" href="https://learn.chatgpt.com/docs/build-skills">Build skills</a>。</p><p>两者的区别可以压缩成：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">AGENTS.md：在这个项目里长期遵守什么</span><br><span class="line">Skill：遇到这一类任务时通常怎样完成</span><br></pre></td></tr></tbody></table></figure><p>项目规则与专项流程可以同时生效，Skill 不能绕过更高优先级的权限和项目约束。</p><h2 id="Harness-不是一个-Markdown-文件"><a href="#Harness-不是一个-Markdown-文件" class="headerlink" title="Harness 不是一个 Markdown 文件"></a>Harness 不是一个 Markdown 文件</h2><p>Agent Harness 是运行 Agent 的外部执行系统。它通常负责：</p><figure class="highlight text"><table><tbody><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><br><span class="line">→ 发现项目规则与可用 Skill</span><br><span class="line">→ 组装 Context</span><br><span class="line">→ 把工具 Schema 提供给模型</span><br><span class="line">→ 校验并执行 Tool Call</span><br><span class="line">→ 返回执行结果</span><br><span class="line">→ 控制循环、预算和停止条件</span><br></pre></td></tr></tbody></table></figure><p>Harness 还会涉及文件和网络沙箱、用户审批、任务状态、日志、超时、重试以及并行任务管理。它不等同于某个统一的 <code>HARNESS.md</code> 标准。</p><p>文字指令告诉 Agent 应该怎样做，操作系统权限和 Harness 策略决定它实际上能不能做。即使某个 Skill 要求联网，也不能绕过网络限制。</p><h2 id="Tool、MCP-和-RAG-怎样配合"><a href="#Tool、MCP-和-RAG-怎样配合" class="headerlink" title="Tool、MCP 和 RAG 怎样配合"></a>Tool、MCP 和 RAG 怎样配合</h2><p>假设任务变成：</p><blockquote><p>根据公司登录安全规范，修复 GitHub 第 123 号 Issue，并创建 PR。</p></blockquote><p>可以拆成：</p><figure class="highlight text"><table><tbody><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">通过 GitHub MCP 读取 Issue</span><br><span class="line">→ 通过 RAG 找到登录失败锁定规范</span><br><span class="line">→ 使用本地文件 Tool 修改代码</span><br><span class="line">→ 使用 Shell Tool 运行测试</span><br><span class="line">→ 人工审查 Diff</span><br><span class="line">→ 通过 GitHub MCP 创建 PR</span><br></pre></td></tr></tbody></table></figure><p>这里，RAG 提供“失败多少次、锁定多久”的知识；MCP 提供连接 GitHub 的方式；Tool 执行读写和测试；Agent 负责组织步骤；Harness 负责权限、反馈和停止。</p><h2 id="怎样给编程-Agent-下任务"><a href="#怎样给编程-Agent-下任务" class="headerlink" title="怎样给编程 Agent 下任务"></a>怎样给编程 Agent 下任务</h2><p>一个高质量任务至少包含四部分：</p><h3 id="Goal"><a href="#Goal" class="headerlink" title="Goal"></a>Goal</h3><p>明确最终行为，而不是只说“优化一下”。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">修复登录按钮在请求失败后一直保持 loading 的问题。</span><br></pre></td></tr></tbody></table></figure><h3 id="Context"><a href="#Context" class="headerlink" title="Context"></a>Context</h3><p>提供入口、复现方式和已知现象。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">入口在 src/pages/login，接口返回 500 时可以稳定复现。</span><br></pre></td></tr></tbody></table></figure><h3 id="Constraints"><a href="#Constraints" class="headerlink" title="Constraints"></a>Constraints</h3><p>说明范围和不能破坏的行为。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">不要更换请求库，不修改接口契约，保留现有视觉样式。</span><br></pre></td></tr></tbody></table></figure><h3 id="Done-when"><a href="#Done-when" class="headerlink" title="Done when"></a>Done when</h3><p>给出可验证完成标准。</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">失败后 loading 恢复，错误提示出现，相关单元测试和类型检查通过。</span><br></pre></td></tr></tbody></table></figure><h2 id="人仍然负责什么"><a href="#人仍然负责什么" class="headerlink" title="人仍然负责什么"></a>人仍然负责什么</h2><p>Agent 可以加快搜索、样板代码、测试补充、迁移和文档整理，但最终责任没有转移：</p><ul><li>产品负责人定义真实问题和验收标准。</li><li>技术负责人决定架构边界和风险接受。</li><li>开发者审查 Diff、数据迁移、依赖和安全影响。</li><li>团队使用测试、Lint、扫描、灰度和监控做可执行门禁。</li><li>生产密钥、高风险命令和发布权限不能因为“由 AI 操作”而放宽。</li></ul><h2 id="一个团队级闭环"><a href="#一个团队级闭环" class="headerlink" title="一个团队级闭环"></a>一个团队级闭环</h2><figure class="highlight text"><table><tbody><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">清晰 Issue</span><br><span class="line">→ Agent 读取规则与代码</span><br><span class="line">→ 小步修改并运行验证</span><br><span class="line">→ 人与 Agent 共同审查 Diff</span><br><span class="line">→ CI 执行统一门禁</span><br><span class="line">→ 灰度发布与监控</span><br><span class="line">→ 失败案例沉淀为测试、规则或 Skill</span><br></pre></td></tr></tbody></table></figure><p>随着失败案例不断转化为自动检查，Agent 的工作环境会更清晰、更可验证。这比单纯写一段越来越长的 Prompt 更可靠。</p><h2 id="一句话记忆"><a href="#一句话记忆" class="headerlink" title="一句话记忆"></a>一句话记忆</h2><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">模型负责想，Agent 负责组织，Tool 负责做，MCP 负责接系统，</span><br><span class="line">RAG 负责找知识，Skill 提供流程，AGENTS.md 规定项目规则，Harness 负责运行与约束。</span><br></pre></td></tr></tbody></table></figure><p>OpenAI 的 <a target="_blank" rel="noopener" href="https://developers.openai.com/api/docs/guides/agents">Agents SDK 指南</a>也把模型响应与 Agent 运行区分开：前者由应用自行管理循环，后者由 SDK 提供 Agent 循环和生命周期能力。</p>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/ai-coding-agent-system/</id>
    <link href="https://blogs.microcyan.com/posts/ai-coding-agent-system/"/>
    <published>2025-07-21T07:20:00.000Z</published>
    <summary>以修复真实项目 Bug 为例，解释编程 Agent、Tool、Skill、AGENTS.md、MCP、RAG、Memory 与 Agent Harness 的职责边界和协作流程。</summary>
    <title>AI 编程 Agent 的工作方式：Tool、Skill、AGENTS.md 与 Harness</title>
    <updated>2026-09-08T13:01:21.398Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="前端开发" scheme="https://blogs.microcyan.com/categories/frontend/"/>
    <category term="故障排查" scheme="https://blogs.microcyan.com/tags/%E6%95%85%E9%9A%9C%E6%8E%92%E6%9F%A5/"/>
    <category term="TypeScript" scheme="https://blogs.microcyan.com/tags/TypeScript/"/>
    <category term="Vue" scheme="https://blogs.microcyan.com/tags/Vue/"/>
    <category term="Vite" scheme="https://blogs.microcyan.com/tags/Vite/"/>
    <category term="开发指南" scheme="https://blogs.microcyan.com/tags/%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/"/>
    <content>
      <![CDATA[<!-- more --><p>这里整理前端项目里最常见、也最容易反复出现的问题。排查时先看浏览器 Console 第一条报错，再看 Network，再看终端输出。</p><h2 id="Vite-启动后页面白屏"><a href="#Vite-启动后页面白屏" class="headerlink" title="Vite 启动后页面白屏"></a>Vite 启动后页面白屏</h2><p>常见报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Uncaught TypeError: Cannot read properties of undefined</span><br></pre></td></tr></tbody></table></figure><p>或：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Failed to load module script</span><br></pre></td></tr></tbody></table></figure><p>直接处理：</p><ul><li>打开浏览器 Console，看第一条红色错误。</li><li>打开 Network，看 JS、CSS 是否 404。</li><li>如果是部署后白屏，先检查 Vite <code>base</code>。</li><li>如果是本地白屏，先检查入口文件、路由、运行时报错。</li></ul><p>常见原因：</p><ul><li><code>base</code> 和部署路径不一致。</li><li>入口组件运行时报错。</li><li>静态资源路径错误。</li><li>路由 history 模式没有服务端 fallback。</li><li>环境变量为 <code>undefined</code>，接口地址拼错。</li></ul><p>完整处理见：<a href="/posts/frontend-vite/">Vite 使用文档</a>。</p><h2 id="Failed-to-resolve-import"><a href="#Failed-to-resolve-import" class="headerlink" title="Failed to resolve import"></a><code>Failed to resolve import</code></h2><p>报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[plugin:vite:import-analysis] Failed to resolve import "@/components/Header.vue"</span><br></pre></td></tr></tbody></table></figure><p>最直接处理：</p><ol><li>确认文件是否存在。</li><li>确认路径大小写是否一致。</li><li>检查 <code>vite.config.ts</code> alias。</li><li>检查 <code>tsconfig.json</code> paths。</li><li>重启 Vite 和 IDE TypeScript 服务。</li></ol><p><code>vite.config.ts</code>：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { fileURLToPath, <span class="variable constant_">URL</span> } <span class="keyword">from</span> <span class="string">'node:url'</span></span><br><span class="line"><span class="keyword">import</span> { defineConfig } <span class="keyword">from</span> <span class="string">'vite'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">resolve</span>: {</span><br><span class="line">    <span class="attr">alias</span>: {</span><br><span class="line">      <span class="string">'@'</span>: <span class="title function_">fileURLToPath</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">'../src'</span>, <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>))</span><br><span class="line">    }</span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>如果 <code>vite.config.ts</code> 在项目根目录，通常是：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">'@'</span>: <span class="title function_">fileURLToPath</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">'./src'</span>, <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>))</span><br></pre></td></tr></tbody></table></figure><p>路径要按配置文件所在位置调整。</p><h2 id="TypeScript-找不到模块"><a href="#TypeScript-找不到模块" class="headerlink" title="TypeScript 找不到模块"></a>TypeScript 找不到模块</h2><p>报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cannot find module '@/utils/request' or its corresponding type declarations.</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li>Vite 配了 alias，但 TypeScript 没配 <code>paths</code>。</li><li>TypeScript 配了 <code>paths</code>，但 Vite 没配 alias。</li><li>文件不在 <code>include</code> 范围内。</li><li>IDE TypeScript 服务缓存旧配置。</li></ul><p>处理：</p><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">  <span class="attr">"compilerOptions"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line">    <span class="attr">"baseUrl"</span><span class="punctuation">:</span> <span class="string">"."</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"paths"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line">      <span class="attr">"@/*"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="string">"src/*"</span></span><br><span class="line">      <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">}</span></span><br><span class="line">  <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><p>更多 TypeScript 报错见：<a href="/posts/frontend-typescript/">TypeScript 使用文档</a>。</p><h2 id="环境变量读取不到"><a href="#环境变量读取不到" class="headerlink" title="环境变量读取不到"></a>环境变量读取不到</h2><p>报错或现象：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">import.meta.env.VITE_API_BASE_URL is undefined</span><br></pre></td></tr></tbody></table></figure><p>处理：</p><ul><li>前端可用变量必须加 <code>VITE_</code> 前缀。</li><li>修改 <code>.env</code> 后重启开发服务。</li><li>确认使用的是 <code>.env.development</code>、<code>.env.production</code> 还是指定 mode。</li><li>不要在 Vite 前端项目里使用 <code>process.env.xxx</code>。</li></ul><p>示例：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">VITE_API_BASE_URL=https://api.example.com</span><br></pre></td></tr></tbody></table></figure><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> apiBase = <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">env</span>.<span class="property">VITE_API_BASE_URL</span></span><br></pre></td></tr></tbody></table></figure><h2 id="线上接口跨域"><a href="#线上接口跨域" class="headerlink" title="线上接口跨域"></a>线上接口跨域</h2><p>浏览器报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Access to XMLHttpRequest at 'https://api.example.com/user' from origin 'https://www.example.com' has been blocked by CORS policy.</span><br></pre></td></tr></tbody></table></figure><p>直接处理：</p><ul><li>本地开发用 Vite proxy。</li><li>线上用 Nginx 或后端网关做同域代理。</li><li>后端正确响应 <code>OPTIONS</code> 预检请求。</li><li>确认 <code>Access-Control-Allow-Origin</code> 不要和 <code>credentials</code> 配错。</li></ul><p>不要试图只在前端加 header 解决 CORS。CORS 是浏览器安全策略，核心要由服务端响应头处理。</p><h2 id="打包后资源-404"><a href="#打包后资源-404" class="headerlink" title="打包后资源 404"></a>打包后资源 404</h2><p>报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET https://example.com/assets/index-xxx.js 404</span><br></pre></td></tr></tbody></table></figure><p>原因：</p><ul><li>部署目录不是域名根路径，但 Vite <code>base</code> 仍然是 <code>/</code>。</li><li>静态资源没有上传完整。</li><li>Nginx 指向了错误目录。</li><li>GitHub Pages 项目页需要仓库名前缀。</li></ul><p>处理：</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">base</span>: <span class="string">'/repo-name/'</span></span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>如果是自定义域名根目录：</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">base</span>: <span class="string">'/'</span></span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><h2 id="页面样式在本地正常，线上异常怎么办"><a href="#页面样式在本地正常，线上异常怎么办" class="headerlink" title="页面样式在本地正常，线上异常怎么办"></a>页面样式在本地正常，线上异常怎么办</h2><p>现象：</p><p>本地预览正常，部署后样式缺失、图片路径错误或页面资源 404。</p><p>原因：</p><p>静态站点部署到 GitHub Pages 项目页时，资源路径通常需要仓库名作为 base path。</p><p>解决：</p><p>本项目通过 GitHub Actions 自动设置 <code>BASE_PATH</code>。如果使用自定义域名，请在仓库变量中设置 <code>SITE_URL</code> 和 <code>BASE_PATH</code>。</p><h2 id="Tailwind-CSS-或-UnoCSS-样式丢失"><a href="#Tailwind-CSS-或-UnoCSS-样式丢失" class="headerlink" title="Tailwind CSS 或 UnoCSS 样式丢失"></a>Tailwind CSS 或 UnoCSS 样式丢失</h2><p>常见原因：</p><ul><li>入口 CSS 没有引入。</li><li>动态 class 没有被扫描到。</li><li>Tailwind 或 UnoCSS 配置文件没有被识别。</li><li>生产环境 tree-shaking 后删除了看起来“没用”的样式。</li></ul><p>不推荐：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> cls = <span class="string">'text-'</span> + color + <span class="string">'-500'</span></span><br></pre></td></tr></tbody></table></figure><p>推荐：</p><figure class="highlight ts"><table><tbody><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">const</span> colorClass = {</span><br><span class="line">  <span class="attr">success</span>: <span class="string">'text-green-500'</span>,</span><br><span class="line">  <span class="attr">warning</span>: <span class="string">'text-amber-500'</span>,</span><br><span class="line">  <span class="attr">danger</span>: <span class="string">'text-red-500'</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>相关入口：</p><ul><li><a href="/posts/frontend-tailwindcss/">Tailwind CSS 使用文档</a></li><li><a href="/posts/frontend-unocss/">UnoCSS 使用文档</a></li></ul><h2 id="Vue-页面数据更新了但视图没变"><a href="#Vue-页面数据更新了但视图没变" class="headerlink" title="Vue 页面数据更新了但视图没变"></a>Vue 页面数据更新了但视图没变</h2><p>常见原因：</p><ul><li>Vue 2 中直接新增对象属性。</li><li>修改了非响应式对象。</li><li>数组更新方式不对。</li><li>组件 key 不稳定，导致状态复用异常。</li></ul><p>Vue 2 老项目：</p><figure class="highlight javascript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">this</span>.$set(<span class="variable language_">this</span>.<span class="property">form</span>, <span class="string">'name'</span>, <span class="string">'Leroi'</span>)</span><br></pre></td></tr></tbody></table></figure><p>Vue 3 项目优先检查数据是否来自 <code>ref</code>、<code>reactive</code>，以及模板里是否正确使用。</p><h2 id="后台管理模板依赖安装失败"><a href="#后台管理模板依赖安装失败" class="headerlink" title="后台管理模板依赖安装失败"></a>后台管理模板依赖安装失败</h2><p>常见报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Node Sass does not yet support your current environment</span><br></pre></td></tr></tbody></table></figure><p>或：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm ERR! code ERESOLVE</span><br></pre></td></tr></tbody></table></figure><p>常见于 <code>vue-element-admin</code>、<code>vue-admin-template</code>、<code>mall-admin-web</code>、<code>iview-admin</code> 这类 Vue 2 老后台模板。</p><p>直接处理：</p><ul><li>固定 Node 版本，老项目优先 Node 14 或 Node 16。</li><li>保留项目原来的 lock 文件。</li><li>不要直接升级 Webpack、loader、<code>node-sass</code> 大版本。</li><li>npm peer 依赖冲突时可以临时使用 <code>npm install --legacy-peer-deps</code>。</li></ul><p>完整处理见：<a href="/posts/frontend-admin-templates/">后台管理模板选型</a>。</p><h2 id="后台菜单不显示怎么办"><a href="#后台菜单不显示怎么办" class="headerlink" title="后台菜单不显示怎么办"></a>后台菜单不显示怎么办</h2><p>常见原因：</p><ul><li>登录成功但用户信息接口失败。</li><li>token 没有带到后续请求。</li><li>当前用户没有角色或菜单权限。</li><li>后端菜单字段和前端路由转换规则不一致。</li><li>路由里配置了 <code>hidden</code>。</li><li>动态路由添加后没有重新跳转。</li></ul><p>排查顺序：</p><ol><li>看登录接口返回。</li><li>看 token 是否保存。</li><li>看用户信息接口是否成功。</li><li>看菜单接口是否返回数据。</li><li>打印最终生成的路由表。</li></ol><p>相关入口：</p><ul><li><a href="/posts/frontend-vue-element-admin/">vue-element-admin 与 vue-admin-template</a></li><li><a href="/posts/frontend-mall-admin-web/">mall-admin-web</a></li><li><a href="/posts/frontend-geeker-admin/">Geeker Admin</a></li><li><a href="/posts/frontend-iview-admin/">iview-admin</a></li></ul><h2 id="Element-UI-或-Element-Plus-样式异常"><a href="#Element-UI-或-Element-Plus-样式异常" class="headerlink" title="Element UI 或 Element Plus 样式异常"></a>Element UI 或 Element Plus 样式异常</h2><p>常见原因：</p><ul><li>组件库版本和 Vue 版本不匹配。</li><li>样式文件没有引入。</li><li>按需引入插件配置不完整。</li><li>自定义主题覆盖顺序错误。</li><li>暗黑模式下只改了背景，文字颜色没有跟着改。</li></ul><p>处理：</p><ul><li>Vue 2 项目用 Element UI。</li><li>Vue 3 项目用 Element Plus。</li><li>检查样式入口。</li><li>不要混用两套组件库。</li></ul><h2 id="小程序真机和开发者工具不一致"><a href="#小程序真机和开发者工具不一致" class="headerlink" title="小程序真机和开发者工具不一致"></a>小程序真机和开发者工具不一致</h2><p>常见原因：</p><ul><li>开发者工具勾选了“不校验合法域名”。</li><li>真机基础库版本不同。</li><li>域名白名单没有配置。</li><li>权限、授权、支付、订阅消息只能在真机完整验证。</li><li>H5 写法被带到小程序端。</li></ul><p>相关入口：</p><ul><li><a href="/posts/frontend-wechat-miniprogram-issues/">微信小程序开发问题</a></li><li><a href="/posts/frontend-alipay-miniprogram-issues/">支付宝小程序开发问题</a></li><li><a href="/posts/frontend-douyin-miniprogram-issues/">抖音小程序开发问题</a></li></ul><h2 id="为什么列表渲染要使用稳定标识"><a href="#为什么列表渲染要使用稳定标识" class="headerlink" title="为什么列表渲染要使用稳定标识"></a>为什么列表渲染要使用稳定标识</h2><p>现象：</p><p>列表新增、删除或排序后，输入框内容、选中状态或局部组件状态错位。</p><p>原因：</p><p>前端框架需要通过 key 判断列表项身份。如果 key 不稳定，框架可能复用错误的节点。</p><p>解决：</p><p>优先使用业务 <code>id</code>。如果数据没有稳定 <code>id</code>，并且列表不会重排、插入或删除，可以使用 <code>index</code>。</p><h2 id="文档页面应该放组件还是-Markdown"><a href="#文档页面应该放组件还是-Markdown" class="headerlink" title="文档页面应该放组件还是 Markdown"></a>文档页面应该放组件还是 Markdown</h2><p>优先使用 Markdown。只有当页面需要复杂交互、特殊可视化或复用业务组件时，再引入 Vue 组件。</p><p>文档站的核心价值是内容可读、可搜索、可维护。过多交互会增加维护成本，也可能影响搜索引擎理解正文内容。</p><h2 id="前端排查顺序"><a href="#前端排查顺序" class="headerlink" title="前端排查顺序"></a>前端排查顺序</h2><ol><li>浏览器 Console 第一条错误。</li><li>Network 里 JS、CSS、接口请求状态码。</li><li>终端里 Vite 或构建工具报错。</li><li>检查环境变量和接口 base URL。</li><li>检查 alias、路径大小写和真实文件。</li><li>清缓存，例如 <code>node_modules/.vite</code>。</li><li>缩小到最小页面或最小组件。</li></ol>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/faq-frontend/</id>
    <link href="https://blogs.microcyan.com/posts/faq-frontend/"/>
    <published>2025-07-15T04:45:00.000Z</published>
    <summary>前端开发中关于 TypeScript、Vite、Vue、后台管理模板、构建、部署、样式、接口请求、路径别名和页面白屏的常见问题。</summary>
    <title>前端常见问题</title>
    <updated>2026-09-08T10:34:43.297Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="前端开发" scheme="https://blogs.microcyan.com/categories/frontend/"/>
    <category term="前端开发" scheme="https://blogs.microcyan.com/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Web 开发" scheme="https://blogs.microcyan.com/tags/Web-%E5%BC%80%E5%8F%91/"/>
    <category term="Electron" scheme="https://blogs.microcyan.com/tags/Electron/"/>
    <content>
      <![CDATA[<!-- more --><p>Electron 用于开发桌面端应用。维护 Electron 项目时，要同时关注 Chromium、Node.js、V8、系统权限、打包签名和安全配置。</p><h2 id="Electron-版本怎么选"><a href="#Electron-版本怎么选" class="headerlink" title="Electron 版本怎么选"></a>Electron 版本怎么选</h2><p>新项目优先使用当前稳定版本。老项目升级时，不要只看 Electron 版本号，还要检查：</p><ul><li>Chromium 版本变化。</li><li>Node.js 版本变化。</li><li>原生模块是否需要重编译。</li><li>打包工具是否兼容。</li><li>macOS、Windows 权限和签名要求。</li></ul><h2 id="主进程和渲染进程怎么区分"><a href="#主进程和渲染进程怎么区分" class="headerlink" title="主进程和渲染进程怎么区分"></a>主进程和渲染进程怎么区分</h2><p>主进程负责窗口、菜单、系统能力、应用生命周期。</p><p>渲染进程负责页面 UI。</p><p>不要把所有能力都塞进渲染进程，涉及系统能力时应通过 preload 和 IPC 暴露有限接口。</p><h2 id="contextIsolation-要不要开启"><a href="#contextIsolation-要不要开启" class="headerlink" title="contextIsolation 要不要开启"></a>contextIsolation 要不要开启</h2><p>建议开启。关闭隔离会增加安全风险。</p><p>preload 示例：</p><figure class="highlight javascript"><table><tbody><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">const</span> { contextBridge, ipcRenderer } = <span class="built_in">require</span>(<span class="string">'electron'</span>)</span><br><span class="line"></span><br><span class="line">contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">'api'</span>, {</span><br><span class="line">  <span class="attr">ping</span>: <span class="function">() =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">'ping'</span>)</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>渲染进程使用：</p><figure class="highlight javascript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">window</span>.<span class="property">api</span>.<span class="title function_">ping</span>()</span><br></pre></td></tr></tbody></table></figure><h2 id="IPC-通信怎么写"><a href="#IPC-通信怎么写" class="headerlink" title="IPC 通信怎么写"></a>IPC 通信怎么写</h2><p>主进程：</p><figure class="highlight javascript"><table><tbody><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">ipcMain.<span class="title function_">handle</span>(<span class="string">'ping'</span>, <span class="title function_">async</span> () =&gt; {</span><br><span class="line">  <span class="keyword">return</span> <span class="string">'pong'</span></span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>preload：</p><figure class="highlight javascript"><table><tbody><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">contextBridge.<span class="title function_">exposeInMainWorld</span>(<span class="string">'api'</span>, {</span><br><span class="line">  <span class="attr">ping</span>: <span class="function">() =&gt;</span> ipcRenderer.<span class="title function_">invoke</span>(<span class="string">'ping'</span>)</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><h2 id="打包后白屏怎么办"><a href="#打包后白屏怎么办" class="headerlink" title="打包后白屏怎么办"></a>打包后白屏怎么办</h2><p>常见原因：</p><ul><li>资源路径错误。</li><li>路由使用 history 模式但没有适配。</li><li>打包产物路径不对。</li><li>preload 路径错误。</li><li>开发环境变量没有在生产环境配置。</li></ul><p>先打开 DevTools 看控制台和网络请求，再检查打包后的文件路径。</p><h2 id="原生模块安装失败怎么办"><a href="#原生模块安装失败怎么办" class="headerlink" title="原生模块安装失败怎么办"></a>原生模块安装失败怎么办</h2><p>Electron 的 Node ABI 可能和本机 Node 不一致。需要使用 Electron 对应环境重新编译原生模块。</p><p>排查：</p><ul><li>Node 版本。</li><li>Electron 版本。</li><li>操作系统和 CPU 架构。</li><li>是否需要 <code>electron-rebuild</code>。</li></ul><h2 id="自动更新要注意什么"><a href="#自动更新要注意什么" class="headerlink" title="自动更新要注意什么"></a>自动更新要注意什么</h2><ul><li>Windows 和 macOS 更新机制不同。</li><li>macOS 通常还涉及签名、公证。</li><li>更新包地址要稳定。</li><li>更新失败要有回退策略。</li><li>不要在用户关键操作中强制重启。</li></ul><h2 id="官方入口"><a href="#官方入口" class="headerlink" title="官方入口"></a>官方入口</h2><ul><li>Electron 文档：<a target="_blank" rel="noopener" href="https://www.electronjs.org/docs/latest/">https://www.electronjs.org/docs/latest/</a></li><li>Electron Releases：<a target="_blank" rel="noopener" href="https://releases.electronjs.org/">https://releases.electronjs.org/</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/frontend-electron/</id>
    <link href="https://blogs.microcyan.com/posts/frontend-electron/"/>
    <published>2025-07-13T03:35:00.000Z</published>
    <summary>Electron 主进程、渲染进程、preload、IPC、打包、自动更新、原生模块和版本升级常见问题。</summary>
    <title>Electron 常见问题</title>
    <updated>2026-09-08T10:34:43.297Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="物联网与机器人" scheme="https://blogs.microcyan.com/categories/iot-robotics/"/>
    <category term="物联网" scheme="https://blogs.microcyan.com/tags/%E7%89%A9%E8%81%94%E7%BD%91/"/>
    <category term="Digital Twin" scheme="https://blogs.microcyan.com/tags/Digital-Twin/"/>
    <category term="OTA" scheme="https://blogs.microcyan.com/tags/OTA/"/>
    <category term="设备安全" scheme="https://blogs.microcyan.com/tags/%E8%AE%BE%E5%A4%87%E5%AE%89%E5%85%A8/"/>
    <content>
      <![CDATA[<!-- more --><p>设备平台不能只保存“最后在线时间”。它需要识别每台设备、理解云端期望状态和设备真实状态的差异，并安全地维护固件直到设备退役。</p><h2 id="设备身份"><a href="#设备身份" class="headerlink" title="设备身份"></a>设备身份</h2><p>每台设备应有稳定唯一标识和独立凭据：</p><figure class="highlight text"><table><tbody><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">device_id：业务与平台身份</span><br><span class="line">certificate/key：加密身份材料</span><br><span class="line">tenant/product：所属租户和产品</span><br><span class="line">policy：允许连接、发布和订阅的范围</span><br></pre></td></tr></tbody></table></figure><p>设备序列号不一定是秘密，不能单独作为认证凭据。生产写入、首次激活、轮换、吊销和报废都要有记录。</p><h2 id="Digital-Twin-或-Device-Shadow"><a href="#Digital-Twin-或-Device-Shadow" class="headerlink" title="Digital Twin 或 Device Shadow"></a>Digital Twin 或 Device Shadow</h2><p>最实用的模型是区分期望状态与上报状态：</p><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">  <span class="attr">"desired"</span><span class="punctuation">:</span> <span class="punctuation">{</span> <span class="attr">"targetTemperature"</span><span class="punctuation">:</span> <span class="number">24</span> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"reported"</span><span class="punctuation">:</span> <span class="punctuation">{</span> <span class="attr">"targetTemperature"</span><span class="punctuation">:</span> <span class="number">26</span><span class="punctuation">,</span> <span class="attr">"online"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">"version"</span><span class="punctuation">:</span> <span class="number">18</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><figure class="highlight text"><table><tbody><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">用户修改 desired</span><br><span class="line">  -&gt; 平台生成命令</span><br><span class="line">  -&gt; 设备执行</span><br><span class="line">  -&gt; 设备重新上报 reported</span><br><span class="line">  -&gt; 平台比较差异并确认收敛</span><br></pre></td></tr></tbody></table></figure><p>Twin 不是设备数据库的漂亮别名。它要定义版本、冲突、过期、离线设备、权限和最终一致性。</p><h2 id="OTA-完整流程"><a href="#OTA-完整流程" class="headerlink" title="OTA 完整流程"></a>OTA 完整流程</h2><figure class="highlight text"><table><tbody><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">构建固件 -&gt; 生成哈希和签名 -&gt; 上传制品</span><br><span class="line">  -&gt; 创建升级任务 -&gt; 选择小批设备灰度</span><br><span class="line">  -&gt; 设备校验型号、版本、空间和电量</span><br><span class="line">  -&gt; 分块下载并验证签名</span><br><span class="line">  -&gt; 安装、重启、自检</span><br><span class="line">  -&gt; 上报结果</span><br><span class="line">  -&gt; 指标正常后逐步扩大批次</span><br></pre></td></tr></tbody></table></figure><p>安全启动和固件签名防止运行未经授权的代码。设备应保留可回滚分区或恢复模式，云端要有暂停、失败阈值和人工介入能力。</p><h2 id="全生命周期安全"><a href="#全生命周期安全" class="headerlink" title="全生命周期安全"></a>全生命周期安全</h2><div class="md-table-scroll"><table><thead><tr><th>阶段</th><th>关键控制</th></tr></thead><tbody><tr><td>设计</td><td>威胁建模、最小权限、安全更新能力</td></tr><tr><td>生产</td><td>每设备唯一身份、安全写入密钥</td></tr><tr><td>激活</td><td>引导凭据一次性或短期化</td></tr><tr><td>运行</td><td>TLS、授权、日志、异常检测、密钥轮换</td></tr><tr><td>更新</td><td>固件签名、灰度、回滚、防降级</td></tr><tr><td>报废</td><td>吊销凭据、清除数据、解除租户绑定</td></tr></tbody></table></div><p>物联网安全还要考虑物理接触、调试口、弱网、长期无人维护和供应链，不能只套用普通 Web 登录方案。</p><h2 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h2><ul><li><a target="_blank" rel="noopener" href="https://csrc.nist.gov/pubs/ir/8259/final">NISTIR 8259：IoT Device Cybersecurity Capability Core Baseline</a></li><li><a target="_blank" rel="noopener" href="https://csrc.nist.gov/pubs/ir/8259/a/final">NISTIR 8259A</a></li><li><a target="_blank" rel="noopener" href="https://datatracker.ietf.org/wg/suit/about/">IETF SUIT Working Group</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/iot-device-twin-ota-security/</id>
    <link href="https://blogs.microcyan.com/posts/iot-device-twin-ota-security/"/>
    <published>2025-07-13T02:56:00.000Z</published>
    <summary>解释设备影子如何同步期望与上报状态、OTA 如何分批升级，以及从生产到报废的设备安全控制。</summary>
    <title>设备身份、Digital Twin、OTA 与安全生命周期</title>
    <updated>2026-09-08T13:56:02.684Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="前端开发" scheme="https://blogs.microcyan.com/categories/frontend/"/>
    <category term="Vue" scheme="https://blogs.microcyan.com/tags/Vue/"/>
    <category term="前端开发" scheme="https://blogs.microcyan.com/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Web 开发" scheme="https://blogs.microcyan.com/tags/Web-%E5%BC%80%E5%8F%91/"/>
    <category term="Element Plus" scheme="https://blogs.microcyan.com/tags/Element-Plus/"/>
    <content>
      <![CDATA[<!-- more --><p>Element Plus 主要用于 Vue 3 项目。它不是 Element UI 2.x 的简单改名，组件 API、图标、类型和主题配置都有差异。</p><h2 id="图标不显示"><a href="#图标不显示" class="headerlink" title="图标不显示"></a>图标不显示</h2><p>Element Plus 图标需要单独安装和引入。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install @element-plus/icons-vue</span><br></pre></td></tr></tbody></table></figure><p>使用：</p><figure class="highlight plaintext"><table><tbody><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">&lt;script setup&gt;</span><br><span class="line">  import { Search } from '@element-plus/icons-vue'</span><br><span class="line">&lt;/script&gt;</span><br><span class="line">&lt;template&gt;</span><br><span class="line">  &lt;el-icon&gt;</span><br><span class="line">    &lt;Search /&gt;</span><br><span class="line">  &lt;/el-icon&gt;</span><br><span class="line">&lt;/template&gt;</span><br></pre></td></tr></tbody></table></figure><h2 id="表单校验类型报错"><a href="#表单校验类型报错" class="headerlink" title="表单校验类型报错"></a>表单校验类型报错</h2><p>Vue 3 + TypeScript 项目建议给表单和 rules 明确类型。</p><figure class="highlight html"><table><tbody><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">interface Form {</span><br><span class="line">name: string</span><br><span class="line">}</span><br><span class="line">const form = reactive</span><br><span class="line"><span class="tag">&lt;<span class="name">Form</span>&gt;</span></span><br><span class="line">  ({</span><br><span class="line">  name: ''</span><br><span class="line">  })</span><br></pre></td></tr></tbody></table></figure><h2 id="按需引入怎么做"><a href="#按需引入怎么做" class="headerlink" title="按需引入怎么做"></a>按需引入怎么做</h2><p>Vite 项目通常使用自动导入插件减少手动引入成本。要注意自动导入配置和样式导入是否完整。</p><p>如果组件能用但样式丢失，多半是样式没有正确引入。</p><h2 id="表格高度或列宽异常"><a href="#表格高度或列宽异常" class="headerlink" title="表格高度或列宽异常"></a>表格高度或列宽异常</h2><p>常见原因：</p><ul><li>父容器高度不稳定。</li><li>弹窗或 tabs 内初始化时不可见。</li><li>数据异步更新后布局未刷新。</li></ul><p>可以在展示后重新计算布局，或给容器明确高度。</p><h2 id="从-Element-UI-迁移要注意什么"><a href="#从-Element-UI-迁移要注意什么" class="headerlink" title="从 Element UI 迁移要注意什么"></a>从 Element UI 迁移要注意什么</h2><p>重点检查：</p><ul><li>图标体系变化。</li><li>表单和表格 API 差异。</li><li><code>v-model</code> 写法变化。</li><li>插槽语法变化。</li><li>主题变量和样式覆盖方式变化。</li><li>TypeScript 类型约束。</li></ul><p>不要直接批量替换组件名，最好按页面逐步迁移。</p><h2 id="官方入口"><a href="#官方入口" class="headerlink" title="官方入口"></a>官方入口</h2><ul><li>Element Plus 文档：<a target="_blank" rel="noopener" href="https://element-plus.org/">https://element-plus.org/</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/frontend-element-plus/</id>
    <link href="https://blogs.microcyan.com/posts/frontend-element-plus/"/>
    <published>2025-07-11T03:02:00.000Z</published>
    <summary>Element Plus 在 Vue 3 项目中的表单、表格、图标、按需引入、主题定制和迁移常见问题。</summary>
    <title>Element Plus 常见问题</title>
    <updated>2026-09-08T10:34:43.297Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="前端开发" scheme="https://blogs.microcyan.com/categories/frontend/"/>
    <category term="Vue" scheme="https://blogs.microcyan.com/tags/Vue/"/>
    <category term="Vite" scheme="https://blogs.microcyan.com/tags/Vite/"/>
    <category term="前端开发" scheme="https://blogs.microcyan.com/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Web 开发" scheme="https://blogs.microcyan.com/tags/Web-%E5%BC%80%E5%8F%91/"/>
    <category term="UnoCSS" scheme="https://blogs.microcyan.com/tags/UnoCSS/"/>
    <content>
      <![CDATA[<!-- more --><p>UnoCSS 是按需即时生成的原子化 CSS 引擎。它不是 Tailwind 的简单替代品，而是更偏“可配置 CSS 引擎”的方案。适合熟悉原子化 CSS、希望轻量、可扩展、按需生成样式的项目。</p><h2 id="UnoCSS-和-Tailwind-CSS-怎么选"><a href="#UnoCSS-和-Tailwind-CSS-怎么选" class="headerlink" title="UnoCSS 和 Tailwind CSS 怎么选"></a>UnoCSS 和 Tailwind CSS 怎么选</h2><div class="md-table-scroll"><table><thead><tr><th>需求</th><th>建议</th></tr></thead><tbody><tr><td>团队想要官方生态和成熟规范</td><td>Tailwind CSS</td></tr><tr><td>想要极轻、可扩展、按需生成</td><td>UnoCSS</td></tr><tr><td>需要图标预设、属性化模式、快捷规则</td><td>UnoCSS</td></tr><tr><td>团队原子化经验不足</td><td>先用 Tailwind 或组件库更稳</td></tr></tbody></table></div><p>UnoCSS 的自由度更高，也意味着团队更需要约束。</p><h2 id="Vite-安装"><a href="#Vite-安装" class="headerlink" title="Vite 安装"></a>Vite 安装</h2><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -D unocss</span><br></pre></td></tr></tbody></table></figure><p><code>vite.config.ts</code>：</p><figure class="highlight ts"><table><tbody><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">import</span> { defineConfig } <span class="keyword">from</span> <span class="string">'vite'</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">UnoCSS</span> <span class="keyword">from</span> <span class="string">'unocss/vite'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">plugins</span>: [</span><br><span class="line">    <span class="title class_">UnoCSS</span>()</span><br><span class="line">  ]</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>入口文件引入：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">'virtual:uno.css'</span></span><br></pre></td></tr></tbody></table></figure><p><code>uno.config.ts</code>：</p><figure class="highlight ts"><table><tbody><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">import</span> { defineConfig, presetWind3 } <span class="keyword">from</span> <span class="string">'unocss'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">presets</span>: [</span><br><span class="line">    <span class="title function_">presetWind3</span>()</span><br><span class="line">  ]</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><h2 id="shortcuts"><a href="#shortcuts" class="headerlink" title="shortcuts"></a>shortcuts</h2><p>高频组合建议写成 shortcuts，避免页面 class 太长。</p><figure class="highlight ts"><table><tbody><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">import</span> { defineConfig, presetWind3 } <span class="keyword">from</span> <span class="string">'unocss'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">presets</span>: [</span><br><span class="line">    <span class="title function_">presetWind3</span>()</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">shortcuts</span>: {</span><br><span class="line">    <span class="string">'app-card'</span>: <span class="string">'rounded-lg bg-white p-4 shadow-sm'</span>,</span><br><span class="line">    <span class="string">'app-button'</span>: <span class="string">'rounded-md bg-blue-600 px-4 py-2 text-sm text-white'</span></span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>使用：</p><figure class="highlight html"><table><tbody><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="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"app-card"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">button</span> <span class="attr">class</span>=<span class="string">"app-button"</span>&gt;</span>保存<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><h2 id="rules-自定义规则"><a href="#rules-自定义规则" class="headerlink" title="rules 自定义规则"></a>rules 自定义规则</h2><p>如果项目里有固定设计令牌，可以用 rules 扩展。</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">rules</span>: [</span><br><span class="line">    [</span><br><span class="line">      <span class="regexp">/^text-brand-(\d+)$/</span>,</span><br><span class="line">      <span class="function">(<span class="params">[, size]</span>) =&gt;</span> ({</span><br><span class="line">        <span class="attr">color</span>: <span class="string">'#2563eb'</span>,</span><br><span class="line">        <span class="string">'font-size'</span>: <span class="string">`<span class="subst">${size}</span>px`</span></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></tbody></table></figure><p>使用：</p><figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"text-brand-16"</span>&gt;</span>品牌文字<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>自定义规则不要太多，否则后续维护成本会上升。</p><h2 id="Attributify-模式"><a href="#Attributify-模式" class="headerlink" title="Attributify 模式"></a>Attributify 模式</h2><p>Attributify 可以把 class 拆到属性上。是否使用看团队习惯。</p><figure class="highlight html"><table><tbody><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="tag">&lt;<span class="name">button</span> <span class="attr">bg</span>=<span class="string">"blue-600 hover:blue-700"</span> <span class="attr">text</span>=<span class="string">"white sm"</span> <span class="attr">px</span>=<span class="string">"4"</span> <span class="attr">py</span>=<span class="string">"2"</span> <span class="attr">rounded</span>=<span class="string">"md"</span>&gt;</span></span><br><span class="line">  保存</span><br><span class="line"><span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>优点是结构清晰，缺点是团队成员不熟悉时阅读成本会变高。</p><h2 id="图标怎么用"><a href="#图标怎么用" class="headerlink" title="图标怎么用"></a>图标怎么用</h2><p>UnoCSS 常配合图标预设使用。图标类名通常类似：</p><figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">"i-lucide-search text-20px"</span>&gt;</span><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>如果图标不显示，检查：</p><ul><li>是否安装图标预设。</li><li>是否安装对应图标集合。</li><li>类名是否被扫描到。</li><li>是否引入 <code>virtual:uno.css</code>。</li></ul><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="样式不生效"><a href="#样式不生效" class="headerlink" title="样式不生效"></a>样式不生效</h3><p>检查：</p><ul><li>Vite 插件是否配置。</li><li>是否引入 <code>virtual:uno.css</code>。</li><li><code>uno.config.ts</code> 是否被识别。</li><li>preset 是否配置。</li><li>class 是否动态拼接导致无法扫描。</li></ul><h3 id="presetWind3-和-presetUno-怎么选"><a href="#presetWind3-和-presetUno-怎么选" class="headerlink" title="presetWind3 和 presetUno 怎么选"></a><code>presetWind3</code> 和 <code>presetUno</code> 怎么选</h3><p>新项目按当前文档优先使用 <code>presetWind3</code>。旧项目如果已经使用 <code>presetUno</code>，不要为了名字更新强行迁移，先确认类名兼容和构建结果。</p><h3 id="动态-class-丢失"><a href="#动态-class-丢失" class="headerlink" title="动态 class 丢失"></a>动态 class 丢失</h3><p>不推荐：</p><figure class="highlight javascript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> cls = <span class="string">`text-<span class="subst">${color}</span>-500`</span></span><br></pre></td></tr></tbody></table></figure><p>推荐：</p><figure class="highlight javascript"><table><tbody><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">const</span> colorClass = {</span><br><span class="line">  <span class="attr">success</span>: <span class="string">'text-green-500'</span>,</span><br><span class="line">  <span class="attr">warning</span>: <span class="string">'text-amber-500'</span>,</span><br><span class="line">  <span class="attr">danger</span>: <span class="string">'text-red-500'</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="和-Tailwind-冲突"><a href="#和-Tailwind-冲突" class="headerlink" title="和 Tailwind 冲突"></a>和 Tailwind 冲突</h3><p>不要在同一个项目里同时启用 Tailwind 和 UnoCSS，除非非常清楚两者的扫描、reset、类名和构建顺序。一般二选一。</p><h3 id="生产样式比开发少"><a href="#生产样式比开发少" class="headerlink" title="生产样式比开发少"></a>生产样式比开发少</h3><p>通常是扫描范围或动态 class 问题。把关键 class 写成静态字符串，或配置 safelist。</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">safelist</span>: [</span><br><span class="line">    <span class="string">'text-green-500'</span>,</span><br><span class="line">    <span class="string">'text-amber-500'</span>,</span><br><span class="line">    <span class="string">'text-red-500'</span></span><br><span class="line">  ]</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><h2 id="使用建议"><a href="#使用建议" class="headerlink" title="使用建议"></a>使用建议</h2><ul><li>先选 preset，再写 shortcuts。</li><li>设计规范通过 shortcuts 和 theme 收敛。</li><li>不要滥用自定义 rules。</li><li>图标、排版、属性模式按需启用。</li><li>组件库项目里只用 UnoCSS 做布局和局部样式，避免全局冲突。</li></ul><h2 id="官方入口"><a href="#官方入口" class="headerlink" title="官方入口"></a>官方入口</h2><ul><li>UnoCSS 文档：<a target="_blank" rel="noopener" href="https://unocss.dev/">https://unocss.dev/</a></li><li>Vite 集成：<a target="_blank" rel="noopener" href="https://unocss.dev/integrations/vite">https://unocss.dev/integrations/vite</a></li><li>配置文件：<a target="_blank" rel="noopener" href="https://unocss.dev/guide/config-file">https://unocss.dev/guide/config-file</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/frontend-unocss/</id>
    <link href="https://blogs.microcyan.com/posts/frontend-unocss/"/>
    <published>2025-07-09T11:02:00.000Z</published>
    <summary>UnoCSS 在 Vite、Vue、React 项目中的安装、preset、shortcuts、icons、attributify、性能和常见问题整理。</summary>
    <title>UnoCSS 使用文档与常见问题</title>
    <updated>2026-09-08T10:34:43.297Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="前端开发" scheme="https://blogs.microcyan.com/categories/frontend/"/>
    <category term="Vue" scheme="https://blogs.microcyan.com/tags/Vue/"/>
    <category term="Vite" scheme="https://blogs.microcyan.com/tags/Vite/"/>
    <category term="前端开发" scheme="https://blogs.microcyan.com/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Web 开发" scheme="https://blogs.microcyan.com/tags/Web-%E5%BC%80%E5%8F%91/"/>
    <category term="Tailwind CSS" scheme="https://blogs.microcyan.com/tags/Tailwind-CSS/"/>
    <content>
      <![CDATA[<!-- more --><p>Tailwind CSS 是 utility-first 的 CSS 框架。它不是传统组件库，而是一套原子化样式工具。适合快速构建自定义界面，也适合和 Vue、React、Svelte、Laravel、Nuxt 等项目搭配。</p><h2 id="适合什么项目"><a href="#适合什么项目" class="headerlink" title="适合什么项目"></a>适合什么项目</h2><div class="md-table-scroll"><table><thead><tr><th>场景</th><th>建议</th></tr></thead><tbody><tr><td>定制化强的 Web 页面</td><td>适合</td></tr><tr><td>后台系统</td><td>适合，但要封装业务组件</td></tr><tr><td>文档站、官网、活动页</td><td>适合</td></tr><tr><td>uni-app 小程序</td><td>不建议直接照搬 Web 写法</td></tr><tr><td>团队没有统一设计规范</td><td>容易写成一页一种风格</td></tr></tbody></table></div><p>Tailwind 的核心价值是“少写 CSS 文件”，但不等于“不做设计规范”。</p><h2 id="Vite-项目安装"><a href="#Vite-项目安装" class="headerlink" title="Vite 项目安装"></a>Vite 项目安装</h2><p>Tailwind CSS v4 推荐使用 Vite 插件。</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install tailwindcss @tailwindcss/vite</span><br></pre></td></tr></tbody></table></figure><p><code>vite.config.ts</code>：</p><figure class="highlight ts"><table><tbody><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">import</span> { defineConfig } <span class="keyword">from</span> <span class="string">'vite'</span></span><br><span class="line"><span class="keyword">import</span> tailwindcss <span class="keyword">from</span> <span class="string">'@tailwindcss/vite'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">plugins</span>: [</span><br><span class="line">    <span class="title function_">tailwindcss</span>()</span><br><span class="line">  ]</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>入口 CSS：</p><figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">@import</span> <span class="string">"tailwindcss"</span>;</span><br></pre></td></tr></tbody></table></figure><p>页面中使用：</p><figure class="highlight html"><table><tbody><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="tag">&lt;<span class="name">h1</span> <span class="attr">class</span>=<span class="string">"text-3xl font-bold text-slate-900"</span>&gt;</span></span><br><span class="line">  Hello Tailwind</span><br><span class="line"><span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>老项目如果使用 Tailwind v3，可能还有 <code>tailwind.config.js</code>、<code>content</code>、<code>@tailwind base</code> 这套写法。维护时先确认版本。</p><h2 id="Vue-中使用"><a href="#Vue-中使用" class="headerlink" title="Vue 中使用"></a>Vue 中使用</h2><figure class="highlight plaintext"><table><tbody><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">&lt;template&gt;</span><br><span class="line">  &lt;button class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white"&gt;</span><br><span class="line">    保存</span><br><span class="line">  &lt;/button&gt;</span><br><span class="line">&lt;/template&gt;</span><br></pre></td></tr></tbody></table></figure><p>当 class 很长时，建议封装组件，而不是每个页面复制一长串。</p><figure class="highlight plaintext"><table><tbody><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;button class="app-button" :disabled="disabled"&gt;</span><br><span class="line">    &lt;slot /&gt;</span><br><span class="line">  &lt;/button&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">defineProps({</span><br><span class="line">  disabled: Boolean</span><br><span class="line">})</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">.app-button {</span><br><span class="line">  @apply rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white disabled:opacity-50;</span><br><span class="line">}</span><br><span class="line">&lt;/style&gt;</span><br></pre></td></tr></tbody></table></figure><h2 id="响应式写法"><a href="#响应式写法" class="headerlink" title="响应式写法"></a>响应式写法</h2><figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"rounded-lg bg-white p-4 shadow-sm"</span>&gt;</span>A<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"rounded-lg bg-white p-4 shadow-sm"</span>&gt;</span>B<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>常见断点前缀：</p><div class="md-table-scroll"><table><thead><tr><th>前缀</th><th>含义</th></tr></thead><tbody><tr><td><code>sm:</code></td><td>小屏以上</td></tr><tr><td><code>md:</code></td><td>中等屏以上</td></tr><tr><td><code>lg:</code></td><td>大屏以上</td></tr><tr><td><code>xl:</code></td><td>更大屏</td></tr><tr><td><code>2xl:</code></td><td>超大屏</td></tr></tbody></table></div><h2 id="暗黑模式"><a href="#暗黑模式" class="headerlink" title="暗黑模式"></a>暗黑模式</h2><p>常见写法：</p><figure class="highlight html"><table><tbody><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="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100"</span>&gt;</span></span><br><span class="line">  内容</span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>如果暗黑模式文字看不见，通常是只改了背景，没有给文字、边框、阴影同步写 <code>dark:</code> 状态。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="样式没有生效"><a href="#样式没有生效" class="headerlink" title="样式没有生效"></a>样式没有生效</h3><p>排查：</p><ul><li>是否引入了入口 CSS。</li><li>Vite 插件是否配置。</li><li>当前 Tailwind 版本是否和文档写法一致。</li><li>class 是否是动态拼接导致无法扫描。</li><li>组件库样式是否覆盖了 Tailwind。</li></ul><p>不推荐：</p><figure class="highlight javascript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> color = <span class="string">'blue'</span></span><br><span class="line"><span class="keyword">const</span> cls = <span class="string">'bg-'</span> + color + <span class="string">'-500'</span></span><br></pre></td></tr></tbody></table></figure><p>推荐把可能的 class 写完整：</p><figure class="highlight javascript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> buttonClass = active ? <span class="string">'bg-blue-500'</span> : <span class="string">'bg-gray-300'</span></span><br></pre></td></tr></tbody></table></figure><h3 id="class-太长怎么办"><a href="#class-太长怎么办" class="headerlink" title="class 太长怎么办"></a>class 太长怎么办</h3><p>处理方式：</p><ul><li>抽组件。</li><li>使用 <code>@apply</code> 收敛高频组合。</li><li>用设计变量限制颜色和间距。</li><li>不要在业务页面里复制大段 class。</li></ul><h3 id="和组件库冲突怎么办"><a href="#和组件库冲突怎么办" class="headerlink" title="和组件库冲突怎么办"></a>和组件库冲突怎么办</h3><p>Tailwind 和组件库混用时，重点处理：</p><ul><li>reset/preflight 影响。</li><li>class 优先级。</li><li>组件库主题变量。</li><li>暗黑模式策略。</li></ul><p>如果只是后台系统，通常建议：组件库负责基础组件，Tailwind 负责页面布局和局部微调。</p><h3 id="生产环境样式丢失"><a href="#生产环境样式丢失" class="headerlink" title="生产环境样式丢失"></a>生产环境样式丢失</h3><p>通常是动态 class 没被扫描到。把动态值改成白名单式映射。</p><figure class="highlight javascript"><table><tbody><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">const</span> colorMap = {</span><br><span class="line">  <span class="attr">success</span>: <span class="string">'bg-green-500'</span>,</span><br><span class="line">  <span class="attr">warning</span>: <span class="string">'bg-amber-500'</span>,</span><br><span class="line">  <span class="attr">danger</span>: <span class="string">'bg-red-500'</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="使用建议"><a href="#使用建议" class="headerlink" title="使用建议"></a>使用建议</h2><ul><li>先定颜色、字号、间距、圆角规则。</li><li>按钮、输入框、卡片、弹窗要封装。</li><li>页面布局可以直接用 utility class。</li><li>复杂状态不要只靠 class 拼接，必要时拆组件。</li><li>不要把 Tailwind 当成设计规范本身。</li></ul><h2 id="官方入口"><a href="#官方入口" class="headerlink" title="官方入口"></a>官方入口</h2><ul><li>Tailwind CSS 文档：<a target="_blank" rel="noopener" href="https://tailwindcss.com/docs">https://tailwindcss.com/docs</a></li><li>Vite 安装：<a target="_blank" rel="noopener" href="https://tailwindcss.com/docs/installation/using-vite">https://tailwindcss.com/docs/installation/using-vite</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/frontend-tailwindcss/</id>
    <link href="https://blogs.microcyan.com/posts/frontend-tailwindcss/"/>
    <published>2025-07-07T13:07:00.000Z</published>
    <summary>Tailwind CSS 在 Vite、Vue、React 和普通前端项目中的安装、配置、实用类、主题、响应式、生产构建和常见问题整理。</summary>
    <title>Tailwind CSS 使用文档与常见问题</title>
    <updated>2026-09-08T10:34:43.297Z</updated>
  </entry>
  <entry>
    <author>
      <name>刘立陈</name>
    </author>
    <category term="前端开发" scheme="https://blogs.microcyan.com/categories/frontend/"/>
    <category term="Vue" scheme="https://blogs.microcyan.com/tags/Vue/"/>
    <category term="Vite" scheme="https://blogs.microcyan.com/tags/Vite/"/>
    <category term="前端开发" scheme="https://blogs.microcyan.com/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="Web 开发" scheme="https://blogs.microcyan.com/tags/Web-%E5%BC%80%E5%8F%91/"/>
    <content>
      <![CDATA[<!-- more --><p>Vite 是现代前端项目里常见的开发服务和构建工具。它开发时快，生产构建通常基于 Rollup。日常最容易出问题的地方是环境变量、路径别名、代理、base 路径、依赖预构建、CORS、HMR 和静态资源路径。</p><h2 id="Vite-负责什么"><a href="#Vite-负责什么" class="headerlink" title="Vite 负责什么"></a>Vite 负责什么</h2><p>Vite 主要负责：</p><ul><li>本地开发服务。</li><li>模块热更新。</li><li>静态资源处理。</li><li>环境变量加载。</li><li>生产构建。</li><li>依赖预构建。</li><li>插件系统。</li></ul><p>TypeScript 类型检查通常不是 Vite 本身做的。Vue 项目一般配合 <code>vue-tsc</code>，普通 TS 项目可以配合 <code>tsc --noEmit</code>。</p><h2 id="基础配置"><a href="#基础配置" class="headerlink" title="基础配置"></a>基础配置</h2><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { fileURLToPath, <span class="variable constant_">URL</span> } <span class="keyword">from</span> <span class="string">'node:url'</span></span><br><span class="line"><span class="keyword">import</span> { defineConfig } <span class="keyword">from</span> <span class="string">'vite'</span></span><br><span class="line"><span class="keyword">import</span> vue <span class="keyword">from</span> <span class="string">'@vitejs/plugin-vue'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">plugins</span>: [</span><br><span class="line">    <span class="title function_">vue</span>()</span><br><span class="line">  ],</span><br><span class="line">  <span class="attr">resolve</span>: {</span><br><span class="line">    <span class="attr">alias</span>: {</span><br><span class="line">      <span class="string">'@'</span>: <span class="title function_">fileURLToPath</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">'./src'</span>, <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>))</span><br><span class="line">    }</span><br><span class="line">  },</span><br><span class="line">  <span class="attr">server</span>: {</span><br><span class="line">    <span class="attr">host</span>: <span class="string">'0.0.0.0'</span>,</span><br><span class="line">    <span class="attr">port</span>: <span class="number">5173</span></span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>路径别名要同时配置 TypeScript：</p><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">  <span class="attr">"compilerOptions"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line">    <span class="attr">"baseUrl"</span><span class="punctuation">:</span> <span class="string">"."</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">"paths"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line">      <span class="attr">"@/*"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="string">"src/*"</span></span><br><span class="line">      <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">}</span></span><br><span class="line">  <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure><h2 id="环境变量不生效"><a href="#环境变量不生效" class="headerlink" title="环境变量不生效"></a>环境变量不生效</h2><p>报错或现象：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">import.meta.env.VITE_API_BASE_URL is undefined</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li>变量没有 <code>VITE_</code> 前缀。</li><li><code>.env</code> 文件不在项目根目录。</li><li>修改 <code>.env</code> 后没有重启开发服务。</li><li>使用了错误的 mode。</li><li>写成了 <code>process.env</code>。</li></ul><p>正确写法：</p><p><code>.env.development</code>：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">VITE_API_BASE_URL=http://localhost:8000</span><br><span class="line">VITE_APP_NAME=Leroi Docs</span><br></pre></td></tr></tbody></table></figure><p>使用：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> apiBase = <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">env</span>.<span class="property">VITE_API_BASE_URL</span></span><br></pre></td></tr></tbody></table></figure><p>注意：</p><ul><li>Vite 暴露给客户端的自定义变量默认必须以 <code>VITE_</code> 开头。</li><li>环境变量值是字符串。</li><li><code>.env.production</code> 只会在对应 mode 下使用。</li></ul><h2 id="process-is-not-defined"><a href="#process-is-not-defined" class="headerlink" title="process is not defined"></a><code>process is not defined</code></h2><p>报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Uncaught ReferenceError: process is not defined</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li>从 webpack 项目迁移过来，还在用 <code>process.env</code>。</li><li>某个依赖在浏览器端访问 Node.js 的 <code>process</code>。</li></ul><p>直接处理：</p><p>把：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">process.<span class="property">env</span>.<span class="property">VUE_APP_API_BASE_URL</span></span><br></pre></td></tr></tbody></table></figure><p>改为：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">env</span>.<span class="property">VITE_API_BASE_URL</span></span><br></pre></td></tr></tbody></table></figure><p>如果是依赖里使用 <code>process.env.NODE_ENV</code>，可以临时在 Vite 里 define：</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">define</span>: {</span><br><span class="line">    <span class="string">'process.env.NODE_ENV'</span>: <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(process.<span class="property">env</span>.<span class="property">NODE_ENV</span>)</span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>不要把服务端密钥通过 define 注入到前端。</p><h2 id="路径别名失效"><a href="#路径别名失效" class="headerlink" title="路径别名失效"></a>路径别名失效</h2><p>报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Failed to resolve import "@/components/AppHeader.vue"</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li><code>vite.config.ts</code> 没配 alias。</li><li><code>tsconfig.json</code> 配了 alias，但 Vite 没配。</li><li>文件大小写不一致。</li><li>实际文件不存在。</li></ul><p>处理：</p><figure class="highlight ts"><table><tbody><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">import</span> { fileURLToPath, <span class="variable constant_">URL</span> } <span class="keyword">from</span> <span class="string">'node:url'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">resolve</span>: {</span><br><span class="line">    <span class="attr">alias</span>: {</span><br><span class="line">      <span class="string">'@'</span>: <span class="title function_">fileURLToPath</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">'./src'</span>, <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>))</span><br><span class="line">    }</span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>排查：</p><ul><li><code>rg --files src | rg AppHeader</code></li><li>检查 import 路径大小写。</li><li>重启 Vite 开发服务。</li><li>重启 IDE TypeScript 服务。</li></ul><h2 id="页面部署后白屏"><a href="#页面部署后白屏" class="headerlink" title="页面部署后白屏"></a>页面部署后白屏</h2><p>浏览器控制台常见报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html".</span><br></pre></td></tr></tbody></table></figure><p>或者：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET https://example.com/assets/index-xxx.js 404</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li><code>base</code> 配置和部署路径不一致。</li><li>SPA 路由没有 fallback 到 <code>index.html</code>。</li><li>静态资源被部署到错误目录。</li><li>服务器返回了 HTML 错误页，但浏览器按 JS 加载。</li></ul><p>GitHub Pages 项目页常见配置：</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">base</span>: <span class="string">'/repo-name/'</span></span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>如果部署到域名根路径：</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">base</span>: <span class="string">'/'</span></span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>排查：</p><ul><li>打开浏览器 Network，看 JS/CSS 是否 404。</li><li>看资源请求路径是否多了或少了仓库名。</li><li>直接访问 <code>dist/index.html</code> 不等于线上能正常运行，必须用 HTTP 服务预览。</li></ul><h2 id="本地打开-dist-index-html-报-CORS"><a href="#本地打开-dist-index-html-报-CORS" class="headerlink" title="本地打开 dist/index.html 报 CORS"></a>本地打开 <code>dist/index.html</code> 报 CORS</h2><p>报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Access to script at 'file:///.../assets/index.js' from origin 'null' has been blocked by CORS policy.</span><br></pre></td></tr></tbody></table></figure><p>原因：</p><p>生产构建后的文件不要用 <code>file://</code> 直接打开。ES Module、资源加载、路由等都需要 HTTP 环境。</p><p>处理：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx vite preview</span><br></pre></td></tr></tbody></table></figure><p>或者用任意静态 HTTP 服务预览 <code>dist</code>。</p><h2 id="代理不生效"><a href="#代理不生效" class="headerlink" title="代理不生效"></a>代理不生效</h2><p>现象：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET http://localhost:5173/api/user 404</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li>前端请求路径没有以 <code>/api</code> 开头。</li><li>代理只在开发环境生效，生产环境无效。</li><li>后端 target 地址错误。</li><li><code>rewrite</code> 写错。</li><li>后端接口本身返回 404。</li></ul><p>配置：</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">server</span>: {</span><br><span class="line">    <span class="attr">proxy</span>: {</span><br><span class="line">      <span class="string">'/api'</span>: {</span><br><span class="line">        <span class="attr">target</span>: <span class="string">'http://127.0.0.1:8000'</span>,</span><br><span class="line">        <span class="attr">changeOrigin</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="attr">rewrite</span>: <span class="function"><span class="params">path</span> =&gt;</span> path.<span class="title function_">replace</span>(<span class="regexp">/^\/api/</span>, <span class="string">''</span>)</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></tbody></table></figure><p>前端请求：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">fetch</span>(<span class="string">'/api/user'</span>)</span><br></pre></td></tr></tbody></table></figure><p>排查：</p><ul><li>直接访问后端真实地址是否正常。</li><li>看终端里 Vite 是否有代理请求日志。</li><li>用浏览器 Network 看请求是否仍然发到前端服务。</li><li>记住：生产环境要由 Nginx、后端网关或部署平台处理代理。</li></ul><h2 id="跨域-CORS-报错"><a href="#跨域-CORS-报错" class="headerlink" title="跨域 CORS 报错"></a>跨域 CORS 报错</h2><p>浏览器报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Access to XMLHttpRequest at 'https://api.example.com/user' from origin 'http://localhost:5173' has been blocked by CORS policy.</span><br></pre></td></tr></tbody></table></figure><p>本地开发处理：</p><ul><li>用 Vite proxy。</li><li>或让后端允许本地开发域名。</li></ul><p>生产处理：</p><ul><li>推荐前端和 API 走同域反向代理。</li><li>后端正确处理 <code>OPTIONS</code> 预检请求。</li><li>不要在前端加奇怪 header 增加预检复杂度。</li></ul><h2 id="依赖预构建报错"><a href="#依赖预构建报错" class="headerlink" title="依赖预构建报错"></a>依赖预构建报错</h2><p>常见报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">The following dependencies are imported but could not be resolved</span><br></pre></td></tr></tbody></table></figure><p>或者：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Pre-bundling dependencies: ...</span><br></pre></td></tr></tbody></table></figure><p>长期卡住或失败。</p><p>处理方法：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> -rf node_modules/.vite</span><br></pre></td></tr></tbody></table></figure><p>如果是 pnpm：</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> -rf node_modules/.vite .vite</span><br></pre></td></tr></tbody></table></figure><p>然后重新启动开发服务。</p><p>也可以手动指定优化依赖：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">optimizeDeps</span>: {</span><br><span class="line">    <span class="attr">include</span>: [</span><br><span class="line">      <span class="string">'lodash-es'</span></span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">exclude</span>: [</span><br><span class="line">      <span class="string">'large-esm-only-package'</span></span><br><span class="line">    ]</span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>不要随便把所有依赖都 include。先定位具体失败的包。</p><h2 id="CommonJS-依赖在浏览器报错"><a href="#CommonJS-依赖在浏览器报错" class="headerlink" title="CommonJS 依赖在浏览器报错"></a>CommonJS 依赖在浏览器报错</h2><p>报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Uncaught ReferenceError: require is not defined</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li>依赖只支持 Node.js 或 CommonJS。</li><li>代码里直接写了 <code>require()</code>。</li><li>依赖访问了 <code>fs</code>、<code>path</code> 等 Node 模块。</li></ul><p>处理：</p><ul><li>换浏览器可用版本。</li><li>找 ESM 入口。</li><li>在服务端处理，不要放前端。</li><li>确认包的 <code>browser</code>、<code>module</code> 字段。</li></ul><p>前端不要直接使用 Node 专属模块：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">'node:fs'</span></span><br></pre></td></tr></tbody></table></figure><p>这种代码应该放到 Node 服务端、构建脚本或 Vite 插件里。</p><h2 id="HMR-不更新或频繁刷新"><a href="#HMR-不更新或频繁刷新" class="headerlink" title="HMR 不更新或频繁刷新"></a>HMR 不更新或频繁刷新</h2><p>常见原因：</p><ul><li>文件监听异常。</li><li>Docker、虚拟机、网络盘里开发。</li><li>依赖或插件导致全量刷新。</li><li>组件状态写在模块外部，热更新后状态异常。</li></ul><p>处理：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">server</span>: {</span><br><span class="line">    <span class="attr">watch</span>: {</span><br><span class="line">      <span class="attr">usePolling</span>: <span class="literal">true</span></span><br><span class="line">    }</span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p><code>usePolling</code> 会增加 CPU 占用，只在容器、虚拟机、共享目录场景下使用。</p><h2 id="PostCSS-或-Tailwind-配置不生效"><a href="#PostCSS-或-Tailwind-配置不生效" class="headerlink" title="PostCSS 或 Tailwind 配置不生效"></a>PostCSS 或 Tailwind 配置不生效</h2><p>常见原因：</p><ul><li>配置文件格式和项目模块类型不匹配。</li><li>Tailwind 版本和文档写法不一致。</li><li>CSS 入口没引入。</li><li>插件顺序错误。</li></ul><p>排查：</p><ul><li>确认 <code>package.json</code> 是否 <code>"type": "module"</code>。</li><li>CommonJS 配置可用 <code>.cjs</code>。</li><li>Vite 5/6/7、Tailwind v3/v4 写法不同，先看当前项目版本。</li></ul><h2 id="global-is-not-defined"><a href="#global-is-not-defined" class="headerlink" title="global is not defined"></a><code>global is not defined</code></h2><p>报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Uncaught ReferenceError: global is not defined</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li>浏览器端依赖假设存在 Node.js 的 <code>global</code>。</li></ul><p>临时处理：</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">define</span>: {</span><br><span class="line">    <span class="attr">global</span>: <span class="string">'globalThis'</span></span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>更推荐：换一个浏览器端兼容的包，或者把这段逻辑放到服务端。</p><h2 id="图片路径打包后错误"><a href="#图片路径打包后错误" class="headerlink" title="图片路径打包后错误"></a>图片路径打包后错误</h2><p>常见错误写法：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> url = <span class="string">'/src/assets/logo.png'</span></span><br></pre></td></tr></tbody></table></figure><p>推荐：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> logoUrl <span class="keyword">from</span> <span class="string">'@/assets/logo.png'</span></span><br></pre></td></tr></tbody></table></figure><p>或：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> logoUrl = <span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">'../assets/logo.png'</span>, <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>).<span class="property">href</span></span><br></pre></td></tr></tbody></table></figure><p>公共静态资源放 <code>public</code>：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">public/logo.png</span><br></pre></td></tr></tbody></table></figure><p>使用：</p><figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">"/logo.png"</span> <span class="attr">alt</span>=<span class="string">"Logo"</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure><p>注意 <code>public</code> 下的文件不会经过打包 hash 处理。</p><h2 id="打包后-chunk-太大"><a href="#打包后-chunk-太大" class="headerlink" title="打包后 chunk 太大"></a>打包后 chunk 太大</h2><p>提示：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Some chunks are larger than 500 kBs after minification.</span><br></pre></td></tr></tbody></table></figure><p>处理方向：</p><ul><li>路由懒加载。</li><li>大型编辑器、图表、地图按需加载。</li><li>拆分 vendor chunk。</li><li>不要一次性引入完整图标库。</li></ul><p>路由懒加载：</p><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">UserPage</span> = (<span class="params"></span>) =&gt; <span class="keyword">import</span>(<span class="string">'@/pages/user/index.vue'</span>)</span><br></pre></td></tr></tbody></table></figure><p>手动分包：</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">build</span>: {</span><br><span class="line">    <span class="attr">rollupOptions</span>: {</span><br><span class="line">      <span class="attr">output</span>: {</span><br><span class="line">        <span class="attr">manualChunks</span>: {</span><br><span class="line">          <span class="attr">vue</span>: [<span class="string">'vue'</span>, <span class="string">'vue-router'</span>, <span class="string">'pinia'</span>]</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></tbody></table></figure><p>不要为了消除警告过度切包，先确认首屏加载是否真的慢。</p><h2 id="Vite-启动但外部设备访问不到"><a href="#Vite-启动但外部设备访问不到" class="headerlink" title="Vite 启动但外部设备访问不到"></a>Vite 启动但外部设备访问不到</h2><p>现象：</p><p>手机访问电脑 IP + 端口打不开。</p><p>处理：</p><figure class="highlight ts"><table><tbody><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">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>({</span><br><span class="line">  <span class="attr">server</span>: {</span><br><span class="line">    <span class="attr">host</span>: <span class="string">'0.0.0.0'</span>,</span><br><span class="line">    <span class="attr">port</span>: <span class="number">5173</span></span><br><span class="line">  }</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure><p>排查：</p><ul><li>电脑和手机是否在同一局域网。</li><li>防火墙是否拦截。</li><li>终端输出的 Network 地址是否正确。</li></ul><h2 id="431-Request-Header-Fields-Too-Large"><a href="#431-Request-Header-Fields-Too-Large" class="headerlink" title="431 Request Header Fields Too Large"></a>431 Request Header Fields Too Large</h2><p>报错：</p><figure class="highlight text"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">431 Request Header Fields Too Large</span><br></pre></td></tr></tbody></table></figure><p>常见原因：</p><ul><li>Cookie 太大。</li><li>请求头太大。</li><li>代理或浏览器缓存了过大的 header。</li></ul><p>处理：</p><ul><li>清理当前站点 Cookie。</li><li>检查登录态是否把大量数据塞进 Cookie。</li><li>减少自定义 header。</li></ul><h2 id="排查顺序"><a href="#排查顺序" class="headerlink" title="排查顺序"></a>排查顺序</h2><p>遇到 Vite 问题时，按这个顺序：</p><ol><li>看浏览器 Console 的第一条错误。</li><li>看 Network 里 JS、CSS、接口是否 404 或 CORS。</li><li>看终端 Vite 报错。</li><li>删除 <code>node_modules/.vite</code> 缓存。</li><li>检查 <code>vite.config.ts</code>、<code>tsconfig.json</code>、<code>.env</code>。</li><li>确认依赖版本和文档版本一致。</li><li>把问题缩小到最小页面或最小依赖。</li></ol><h2 id="官方入口"><a href="#官方入口" class="headerlink" title="官方入口"></a>官方入口</h2><ul><li>Vite 文档：<a target="_blank" rel="noopener" href="https://vite.dev/guide/">https://vite.dev/guide/</a></li><li>环境变量与模式：<a target="_blank" rel="noopener" href="https://vite.dev/guide/env-and-mode/">https://vite.dev/guide/env-and-mode/</a></li><li>故障排除：<a target="_blank" rel="noopener" href="https://vite.dev/guide/troubleshooting">https://vite.dev/guide/troubleshooting</a></li></ul>]]>
    </content>
    <id>https://blogs.microcyan.com/posts/frontend-vite/</id>
    <link href="https://blogs.microcyan.com/posts/frontend-vite/"/>
    <published>2025-07-04T13:30:00.000Z</published>
    <summary>Vite 在 Vue、React 和普通前端项目中的安装、环境变量、路径别名、代理、base、打包、依赖预构建、HMR 和常见报错整理。</summary>
    <title>Vite 使用文档与常见问题</title>
    <updated>2026-09-08T10:34:43.297Z</updated>
  </entry>
</feed>
