[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fwfmZL2Ke2M3zkcqX93-1W9Z4N94bkUb_caq1n0w-wZU":3,"$fJU-4tot_gC5fDkujNeoE-cGsdMy5V_KcdUXLuAnTFgw":15,"$f0xhMYeTMXcjGvYkDxi285HuJ5VTeBXma0lpK6ZdE23E":423},{"slug":4,"title":5,"description":6,"content":7,"content_html":8,"pub_date":9,"tags":10,"draft":14},"pwa-install-update-button","PWA 踩坑：为什么安装按钮从来不出现","从 beforeinstallprompt 到 Service Worker waiting，把 PWA 的安装与更新提示真正做对","# PWA 踩坑：为什么安装按钮从来不出现\n\n作为一名前端开发者，你可能曾经满怀期待地在自己的 PWA 应用里写下这段代码：\n\n```javascript\nwindow.addEventListener('beforeinstallprompt', (e) => {\n  e.preventDefault();\n  deferredPrompt = e;\n  showInstallButton();\n});\n```\n\n然后打开浏览器，等待安装按钮出现……然后等了很久，什么都没发生。\n\n这篇文章记录了我在实现 PWA 安装按钮过程中踩过的所有坑，以及最终让它稳定运行的方案。\n\n---\n\n## 背景：PWA 安装的基本原理\n\nProgressive Web App（PWA）允许用户将网页\"安装\"到设备上，像原生 App 一样使用。安装的触发机制依赖浏览器的 **`beforeinstallprompt`** 事件。\n\n整体流程如下：\n\n1. 用户访问网站\n2. 浏览器检查该网站是否满足\"可安装条件\"\n3. 满足条件后，浏览器触发 `beforeinstallprompt` 事件\n4. 开发者捕获该事件，保存并在合适时机调用 `prompt()` 展示安装对话框\n\n听起来很简单，但实际上坑非常多。\n\n---\n\n## 可安装条件（Installability Criteria）\n\nChrome 对 PWA 的可安装条件非常严格，**全部满足**才会触发 `beforeinstallprompt`：\n\n### 1. HTTPS（或 localhost）\n\nPWA 必须运行在安全上下文中。HTTP 站点直接不触发，没有任何商量余地。\n\n```\n✅ https:\u002F\u002Fyoursite.com\n✅ http:\u002F\u002Flocalhost:3000\n❌ http:\u002F\u002Fyoursite.com\n```\n\n### 2. 有效的 Web App Manifest\n\n`manifest.json` 必须满足：\n\n```json\n{\n  \"name\": \"My App\",           \u002F\u002F 必须，非空\n  \"short_name\": \"App\",        \u002F\u002F 推荐\n  \"start_url\": \"\u002F\",           \u002F\u002F 必须\n  \"display\": \"standalone\",    \u002F\u002F 必须是 standalone \u002F fullscreen \u002F minimal-ui\n  \"icons\": [\n    {\n      \"src\": \"\u002Ficons\u002Ficon-192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image\u002Fpng\"\n    },\n    {\n      \"src\": \"\u002Ficons\u002Ficon-512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image\u002Fpng\"\n    }\n  ]\n}\n```\n\n关键要求：\n- `display` 不能是 `browser`，否则不触发\n- 必须包含 **192×192** 和 **512×512** 两个图标\n- 图标必须能正常加载（404 会导致失败）\n- `start_url` 必须在 manifest 的 scope 范围内\n\n### 3. Service Worker\n\n必须注册一个有效的 Service Worker，且该 SW 必须控制当前页面。\n\n```javascript\n\u002F\u002F 最简 Service Worker，能用就行\nself.addEventListener('fetch', (event) => {\n  event.respondWith(fetch(event.request));\n});\n```\n\n注意：SW 必须成功**激活**（状态为 `activated`），仅仅注册还不够。\n\n### 4. 用户参与度（Engagement）\n\n这是最隐蔽的条件。Chrome 要求用户与网站有一定的交互历史，具体算法不公开，但大致包括：\n- 访问过多次\n- 在页面上停留足够长时间\n- 有点击等交互行为\n\n这意味着**第一次访问几乎不会触发**安装提示。\n\n---\n\n## 常见坑点逐一排查\n\n### 坑 1：事件监听器注册太晚\n\n`beforeinstallprompt` 可能在页面加载很早期就触发，如果你在某个按钮点击后才注册监听，事件早就错过了。\n\n```javascript\n\u002F\u002F ❌ 错误：太晚了\ndocument.getElementById('someButton').addEventListener('click', () => {\n  window.addEventListener('beforeinstallprompt', handler); \u002F\u002F 已经错过\n});\n\n\u002F\u002F ✅ 正确：尽早注册\nwindow.addEventListener('beforeinstallprompt', (e) => {\n  e.preventDefault();\n  deferredPrompt = e;\n});\n```\n\n**最佳实践**：在 `\u003Chead>` 的内联 script 中或 HTML 解析的第一个 JS 文件中注册，不要依赖 DOMContentLoaded。\n\n### 坑 2：manifest 没有正确链接\n\n```html\n\u003C!-- ❌ 漏了 crossorigin -->\n\u003Clink rel=\"manifest\" href=\"\u002Fmanifest.json\">\n\n\u003C!-- ✅ 对于跨域资源或需要认证的场景 -->\n\u003Clink rel=\"manifest\" href=\"\u002Fmanifest.json\" crossorigin=\"use-credentials\">\n```\n\n用 Chrome DevTools → Application → Manifest 面板检查是否正确加载，有没有错误信息。\n\n### 坑 3：图标路径 404\n\n这是最容易忽略的问题。Manifest 里写了图标路径，但实际文件不存在。\n\n```bash\n# 检查图标是否可访问\ncurl -I https:\u002F\u002Fyoursite.com\u002Ficons\u002Ficon-192.png\ncurl -I https:\u002F\u002Fyoursite.com\u002Ficons\u002Ficon-512.png\n```\n\n### 坑 4：Service Worker 作用域问题\n\n```javascript\n\u002F\u002F manifest 的 start_url 是 \u002Fapp\u002F\n\u002F\u002F 但 SW 注册在根路径，scope 默认是 \u002Fsw.js 所在目录\n\nnavigator.serviceWorker.register('\u002Fapp\u002Fsw.js')\n\u002F\u002F scope 自动变为 \u002Fapp\u002F，覆盖 start_url ✅\n\nnavigator.serviceWorker.register('\u002Fsw.js')\n\u002F\u002F scope 是 \u002F，同样覆盖 \u002Fapp\u002F ✅\n\nnavigator.serviceWorker.register('\u002Fassets\u002Fsw.js')\n\u002F\u002F scope 是 \u002Fassets\u002F，不覆盖 \u002Fapp\u002F ❌\n```\n\n### 坑 5：已经安装过了\n\n如果用户已经安装过该 PWA，浏览器不会再次触发 `beforeinstallprompt`。\n\n```javascript\n\u002F\u002F 检测是否已在独立模式运行（即已安装）\nif (window.matchMedia('(display-mode: standalone)').matches) {\n  console.log('已安装为 PWA');\n}\n\n\u002F\u002F 或者\nif (window.navigator.standalone === true) {\n  console.log('iOS 上已安装');\n}\n```\n\n### 坑 6：iOS 根本不支持\n\n**iOS Safari 不支持 `beforeinstallprompt` 事件**，这不是 bug，是苹果的设计决策。\n\niOS 上的安装只能通过\"分享 → 添加到主屏幕\"手动操作。你需要专门为 iOS 显示引导提示：\n\n```javascript\nconst isIOS = \u002FiPad|iPhone|iPod\u002F.test(navigator.userAgent);\nconst isInStandaloneMode = window.navigator.standalone === true;\n\nif (isIOS && !isInStandaloneMode) {\n  showIOSInstallGuide(); \u002F\u002F 显示\"请点击分享按钮...\"的提示\n}\n```\n\n---\n\n## 完整可用的安装按钮实现\n\n```javascript\n\u002F\u002F pwa-install.js\n\nlet deferredPrompt = null;\n\n\u002F\u002F 尽早注册，不等 DOMContentLoaded\nwindow.addEventListener('beforeinstallprompt', (e) => {\n  console.log('beforeinstallprompt fired');\n  e.preventDefault();\n  deferredPrompt = e;\n  updateInstallUI();\n});\n\nwindow.addEventListener('appinstalled', () => {\n  console.log('PWA installed');\n  deferredPrompt = null;\n  updateInstallUI();\n});\n\nfunction updateInstallUI() {\n  const btn = document.getElementById('install-btn');\n  if (!btn) return;\n\n  const isInstalled = window.matchMedia('(display-mode: standalone)').matches;\n  const isIOS = \u002FiPad|iPhone|iPod\u002F.test(navigator.userAgent);\n\n  if (isInstalled) {\n    btn.style.display = 'none';\n  } else if (isIOS) {\n    btn.textContent = '添加到主屏幕';\n    btn.style.display = 'block';\n    btn.onclick = showIOSGuide;\n  } else if (deferredPrompt) {\n    btn.textContent = '安装应用';\n    btn.style.display = 'block';\n    btn.onclick = triggerInstall;\n  } else {\n    btn.style.display = 'none'; \u002F\u002F 条件未满足，隐藏\n  }\n}\n\nasync function triggerInstall() {\n  if (!deferredPrompt) return;\n  \n  deferredPrompt.prompt();\n  const { outcome } = await deferredPrompt.userChoice;\n  console.log('User choice:', outcome);\n  deferredPrompt = null;\n  updateInstallUI();\n}\n\nfunction showIOSGuide() {\n  alert('请点击底部的\"分享\"按钮，然后选择\"添加到主屏幕\"');\n}\n\n\u002F\u002F DOM 加载完成后初始化 UI\ndocument.addEventListener('DOMContentLoaded', updateInstallUI);\n```\n\n---\n\n## 调试技巧\n\n### 使用 Chrome DevTools\n\n1. 打开 DevTools → **Application** 标签\n2. **Manifest** 面板：检查 manifest 解析状态，有红色警告就要修复\n3. **Service Workers** 面板：确认 SW 已激活（状态显示 `activated and is running`）\n4. **Storage** → Clear site data：清除状态重新测试\n\n### 强制触发（仅测试用）\n\nChrome 地址栏输入：\n```\nchrome:\u002F\u002Fflags\u002F#bypass-app-banner-engagement-checks\n```\n启用后，绕过用户参与度检查，方便本地调试。\n\n### 使用 Lighthouse\n\n```bash\nnpx lighthouse https:\u002F\u002Fyoursite.com --categories=pwa --view\n```\n\nLighthouse 会检查所有 PWA 可安装条件并给出具体的修复建议。\n\n---\n\n## 注意事项\n\n1. **不要强迫用户安装**：Google 规定不能在用户没有主动交互的情况下调用 `prompt()`，否则会被忽略或降低站点评分\n2. **处理 `userChoice` 后清空**：用户拒绝后 `deferredPrompt` 不能再次使用，必须清空\n3. **测试环境差异**：本地 `localhost` 和生产 HTTPS 行为基本一致，但某些浏览器版本可能有差异\n4. **浏览器兼容性**：`beforeinstallprompt` 目前只有 Chrome\u002FEdge\u002F部分安卓浏览器支持，Firefox 和 Safari 不支持\n\n---\n\n## 总结\n\nPWA 安装按钮不出现，通常是以下原因之一：\n\n| 原因 | 排查方法 |\n|------|---------|\n| 非 HTTPS | 检查 URL |\n| Manifest 无效 | DevTools → Application → Manifest |\n| 图标 404 | curl 验证图标路径 |\n| Service Worker 未激活 | DevTools → Service Workers |\n| 事件监听太晚 | 移到最早的 JS |\n| 用户参与度不足 | 启用 bypass flag 测试 |\n| 已经安装过 | 检查 standalone 模式 |\n| iOS 设备 | 专门处理，引导手动安装 |\n\n按照这个清单逐一排查，基本上能解决 99% 的问题。PWA 的安装机制设计有些繁琐，但一旦理解了背后的逻辑，实现起来并不困难。\n","\u003Ch1>PWA 踩坑：为什么安装按钮从来不出现\u003C\u002Fh1>\n\u003Cp>作为一名前端开发者，你可能曾经满怀期待地在自己的 PWA 应用里写下这段代码：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">window.addEventListener('beforeinstallprompt', (e) =&gt; {\n  e.preventDefault();\n  deferredPrompt = e;\n  showInstallButton();\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然后打开浏览器，等待安装按钮出现……然后等了很久，什么都没发生。\u003C\u002Fp>\n\u003Cp>这篇文章记录了我在实现 PWA 安装按钮过程中踩过的所有坑，以及最终让它稳定运行的方案。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"背景-pwa-安装的基本原理\">背景：PWA 安装的基本原理\u003C\u002Fh2>\n\u003Cp>Progressive Web App（PWA）允许用户将网页&quot;安装&quot;到设备上，像原生 App 一样使用。安装的触发机制依赖浏览器的 \u003Cstrong>\u003Ccode>beforeinstallprompt\u003C\u002Fcode>\u003C\u002Fstrong> 事件。\u003C\u002Fp>\n\u003Cp>整体流程如下：\u003C\u002Fp>\n\u003Col>\n\u003Cli>用户访问网站\u003C\u002Fli>\n\u003Cli>浏览器检查该网站是否满足&quot;可安装条件&quot;\u003C\u002Fli>\n\u003Cli>满足条件后，浏览器触发 \u003Ccode>beforeinstallprompt\u003C\u002Fcode> 事件\u003C\u002Fli>\n\u003Cli>开发者捕获该事件，保存并在合适时机调用 \u003Ccode>prompt()\u003C\u002Fcode> 展示安装对话框\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>听起来很简单，但实际上坑非常多。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"可安装条件-installability-criteria\">可安装条件（Installability Criteria）\u003C\u002Fh2>\n\u003Cp>Chrome 对 PWA 的可安装条件非常严格，\u003Cstrong>全部满足\u003C\u002Fstrong>才会触发 \u003Ccode>beforeinstallprompt\u003C\u002Fcode>：\u003C\u002Fp>\n\u003Ch3 id=\"1-https-或-localhost\">1. HTTPS（或 localhost）\u003C\u002Fh3>\n\u003Cp>PWA 必须运行在安全上下文中。HTTP 站点直接不触发，没有任何商量余地。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>✅ https:\u002F\u002Fyoursite.com\n✅ http:\u002F\u002Flocalhost:3000\n❌ http:\u002F\u002Fyoursite.com\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"2-有效的-web-app-manifest\">2. 有效的 Web App Manifest\u003C\u002Fh3>\n\u003Cp>\u003Ccode>manifest.json\u003C\u002Fcode> 必须满足：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;name&quot;: &quot;My App&quot;,           \u002F\u002F 必须，非空\n  &quot;short_name&quot;: &quot;App&quot;,        \u002F\u002F 推荐\n  &quot;start_url&quot;: &quot;\u002F&quot;,           \u002F\u002F 必须\n  &quot;display&quot;: &quot;standalone&quot;,    \u002F\u002F 必须是 standalone \u002F fullscreen \u002F minimal-ui\n  &quot;icons&quot;: [\n    {\n      &quot;src&quot;: &quot;\u002Ficons\u002Ficon-192.png&quot;,\n      &quot;sizes&quot;: &quot;192x192&quot;,\n      &quot;type&quot;: &quot;image\u002Fpng&quot;\n    },\n    {\n      &quot;src&quot;: &quot;\u002Ficons\u002Ficon-512.png&quot;,\n      &quot;sizes&quot;: &quot;512x512&quot;,\n      &quot;type&quot;: &quot;image\u002Fpng&quot;\n    }\n  ]\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>关键要求：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>display\u003C\u002Fcode> 不能是 \u003Ccode>browser\u003C\u002Fcode>，否则不触发\u003C\u002Fli>\n\u003Cli>必须包含 \u003Cstrong>192×192\u003C\u002Fstrong> 和 \u003Cstrong>512×512\u003C\u002Fstrong> 两个图标\u003C\u002Fli>\n\u003Cli>图标必须能正常加载（404 会导致失败）\u003C\u002Fli>\n\u003Cli>\u003Ccode>start_url\u003C\u002Fcode> 必须在 manifest 的 scope 范围内\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"3-service-worker\">3. Service Worker\u003C\u002Fh3>\n\u003Cp>必须注册一个有效的 Service Worker，且该 SW 必须控制当前页面。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">\u002F\u002F 最简 Service Worker，能用就行\nself.addEventListener('fetch', (event) =&gt; {\n  event.respondWith(fetch(event.request));\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意：SW 必须成功\u003Cstrong>激活\u003C\u002Fstrong>（状态为 \u003Ccode>activated\u003C\u002Fcode>），仅仅注册还不够。\u003C\u002Fp>\n\u003Ch3 id=\"4-用户参与度-engagement\">4. 用户参与度（Engagement）\u003C\u002Fh3>\n\u003Cp>这是最隐蔽的条件。Chrome 要求用户与网站有一定的交互历史，具体算法不公开，但大致包括：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>访问过多次\u003C\u002Fli>\n\u003Cli>在页面上停留足够长时间\u003C\u002Fli>\n\u003Cli>有点击等交互行为\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>这意味着\u003Cstrong>第一次访问几乎不会触发\u003C\u002Fstrong>安装提示。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"常见坑点逐一排查\">常见坑点逐一排查\u003C\u002Fh2>\n\u003Ch3 id=\"坑-1-事件监听器注册太晚\">坑 1：事件监听器注册太晚\u003C\u002Fh3>\n\u003Cp>\u003Ccode>beforeinstallprompt\u003C\u002Fcode> 可能在页面加载很早期就触发，如果你在某个按钮点击后才注册监听，事件早就错过了。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">\u002F\u002F ❌ 错误：太晚了\ndocument.getElementById('someButton').addEventListener('click', () =&gt; {\n  window.addEventListener('beforeinstallprompt', handler); \u002F\u002F 已经错过\n});\n\n\u002F\u002F ✅ 正确：尽早注册\nwindow.addEventListener('beforeinstallprompt', (e) =&gt; {\n  e.preventDefault();\n  deferredPrompt = e;\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>最佳实践\u003C\u002Fstrong>：在 \u003Ccode>&lt;head&gt;\u003C\u002Fcode> 的内联 script 中或 HTML 解析的第一个 JS 文件中注册，不要依赖 DOMContentLoaded。\u003C\u002Fp>\n\u003Ch3 id=\"坑-2-manifest-没有正确链接\">坑 2：manifest 没有正确链接\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;!-- ❌ 漏了 crossorigin --&gt;\n&lt;link rel=&quot;manifest&quot; href=&quot;\u002Fmanifest.json&quot;&gt;\n\n&lt;!-- ✅ 对于跨域资源或需要认证的场景 --&gt;\n&lt;link rel=&quot;manifest&quot; href=&quot;\u002Fmanifest.json&quot; crossorigin=&quot;use-credentials&quot;&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>用 Chrome DevTools → Application → Manifest 面板检查是否正确加载，有没有错误信息。\u003C\u002Fp>\n\u003Ch3 id=\"坑-3-图标路径-404\">坑 3：图标路径 404\u003C\u002Fh3>\n\u003Cp>这是最容易忽略的问题。Manifest 里写了图标路径，但实际文件不存在。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\"># 检查图标是否可访问\ncurl -I https:\u002F\u002Fyoursite.com\u002Ficons\u002Ficon-192.png\ncurl -I https:\u002F\u002Fyoursite.com\u002Ficons\u002Ficon-512.png\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"坑-4-service-worker-作用域问题\">坑 4：Service Worker 作用域问题\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-javascript\">\u002F\u002F manifest 的 start_url 是 \u002Fapp\u002F\n\u002F\u002F 但 SW 注册在根路径，scope 默认是 \u002Fsw.js 所在目录\n\nnavigator.serviceWorker.register('\u002Fapp\u002Fsw.js')\n\u002F\u002F scope 自动变为 \u002Fapp\u002F，覆盖 start_url ✅\n\nnavigator.serviceWorker.register('\u002Fsw.js')\n\u002F\u002F scope 是 \u002F，同样覆盖 \u002Fapp\u002F ✅\n\nnavigator.serviceWorker.register('\u002Fassets\u002Fsw.js')\n\u002F\u002F scope 是 \u002Fassets\u002F，不覆盖 \u002Fapp\u002F ❌\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"坑-5-已经安装过了\">坑 5：已经安装过了\u003C\u002Fh3>\n\u003Cp>如果用户已经安装过该 PWA，浏览器不会再次触发 \u003Ccode>beforeinstallprompt\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">\u002F\u002F 检测是否已在独立模式运行（即已安装）\nif (window.matchMedia('(display-mode: standalone)').matches) {\n  console.log('已安装为 PWA');\n}\n\n\u002F\u002F 或者\nif (window.navigator.standalone === true) {\n  console.log('iOS 上已安装');\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"坑-6-ios-根本不支持\">坑 6：iOS 根本不支持\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>iOS Safari 不支持 \u003Ccode>beforeinstallprompt\u003C\u002Fcode> 事件\u003C\u002Fstrong>，这不是 bug，是苹果的设计决策。\u003C\u002Fp>\n\u003Cp>iOS 上的安装只能通过&quot;分享 → 添加到主屏幕&quot;手动操作。你需要专门为 iOS 显示引导提示：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">const isIOS = \u002FiPad|iPhone|iPod\u002F.test(navigator.userAgent);\nconst isInStandaloneMode = window.navigator.standalone === true;\n\nif (isIOS &amp;&amp; !isInStandaloneMode) {\n  showIOSInstallGuide(); \u002F\u002F 显示&quot;请点击分享按钮...&quot;的提示\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Chr>\n\u003Ch2 id=\"完整可用的安装按钮实现\">完整可用的安装按钮实现\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-javascript\">\u002F\u002F pwa-install.js\n\nlet deferredPrompt = null;\n\n\u002F\u002F 尽早注册，不等 DOMContentLoaded\nwindow.addEventListener('beforeinstallprompt', (e) =&gt; {\n  console.log('beforeinstallprompt fired');\n  e.preventDefault();\n  deferredPrompt = e;\n  updateInstallUI();\n});\n\nwindow.addEventListener('appinstalled', () =&gt; {\n  console.log('PWA installed');\n  deferredPrompt = null;\n  updateInstallUI();\n});\n\nfunction updateInstallUI() {\n  const btn = document.getElementById('install-btn');\n  if (!btn) return;\n\n  const isInstalled = window.matchMedia('(display-mode: standalone)').matches;\n  const isIOS = \u002FiPad|iPhone|iPod\u002F.test(navigator.userAgent);\n\n  if (isInstalled) {\n    btn.style.display = 'none';\n  } else if (isIOS) {\n    btn.textContent = '添加到主屏幕';\n    btn.style.display = 'block';\n    btn.onclick = showIOSGuide;\n  } else if (deferredPrompt) {\n    btn.textContent = '安装应用';\n    btn.style.display = 'block';\n    btn.onclick = triggerInstall;\n  } else {\n    btn.style.display = 'none'; \u002F\u002F 条件未满足，隐藏\n  }\n}\n\nasync function triggerInstall() {\n  if (!deferredPrompt) return;\n  \n  deferredPrompt.prompt();\n  const { outcome } = await deferredPrompt.userChoice;\n  console.log('User choice:', outcome);\n  deferredPrompt = null;\n  updateInstallUI();\n}\n\nfunction showIOSGuide() {\n  alert('请点击底部的&quot;分享&quot;按钮，然后选择&quot;添加到主屏幕&quot;');\n}\n\n\u002F\u002F DOM 加载完成后初始化 UI\ndocument.addEventListener('DOMContentLoaded', updateInstallUI);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Chr>\n\u003Ch2 id=\"调试技巧\">调试技巧\u003C\u002Fh2>\n\u003Ch3 id=\"使用-chrome-devtools\">使用 Chrome DevTools\u003C\u002Fh3>\n\u003Col>\n\u003Cli>打开 DevTools → \u003Cstrong>Application\u003C\u002Fstrong> 标签\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Manifest\u003C\u002Fstrong> 面板：检查 manifest 解析状态，有红色警告就要修复\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Service Workers\u003C\u002Fstrong> 面板：确认 SW 已激活（状态显示 \u003Ccode>activated and is running\u003C\u002Fcode>）\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Storage\u003C\u002Fstrong> → Clear site data：清除状态重新测试\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3 id=\"强制触发-仅测试用\">强制触发（仅测试用）\u003C\u002Fh3>\n\u003Cp>Chrome 地址栏输入：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>chrome:\u002F\u002Fflags\u002F#bypass-app-banner-engagement-checks\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>启用后，绕过用户参与度检查，方便本地调试。\u003C\u002Fp>\n\u003Ch3 id=\"使用-lighthouse\">使用 Lighthouse\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-bash\">npx lighthouse https:\u002F\u002Fyoursite.com --categories=pwa --view\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Lighthouse 会检查所有 PWA 可安装条件并给出具体的修复建议。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"注意事项\">注意事项\u003C\u002Fh2>\n\u003Col>\n\u003Cli>\u003Cstrong>不要强迫用户安装\u003C\u002Fstrong>：Google 规定不能在用户没有主动交互的情况下调用 \u003Ccode>prompt()\u003C\u002Fcode>，否则会被忽略或降低站点评分\u003C\u002Fli>\n\u003Cli>\u003Cstrong>处理 \u003Ccode>userChoice\u003C\u002Fcode> 后清空\u003C\u002Fstrong>：用户拒绝后 \u003Ccode>deferredPrompt\u003C\u002Fcode> 不能再次使用，必须清空\u003C\u002Fli>\n\u003Cli>\u003Cstrong>测试环境差异\u003C\u002Fstrong>：本地 \u003Ccode>localhost\u003C\u002Fcode> 和生产 HTTPS 行为基本一致，但某些浏览器版本可能有差异\u003C\u002Fli>\n\u003Cli>\u003Cstrong>浏览器兼容性\u003C\u002Fstrong>：\u003Ccode>beforeinstallprompt\u003C\u002Fcode> 目前只有 Chrome\u002FEdge\u002F部分安卓浏览器支持，Firefox 和 Safari 不支持\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Chr>\n\u003Ch2 id=\"总结\">总结\u003C\u002Fh2>\n\u003Cp>PWA 安装按钮不出现，通常是以下原因之一：\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>原因\u003C\u002Fth>\n\u003Cth>排查方法\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>非 HTTPS\u003C\u002Ftd>\n\u003Ctd>检查 URL\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Manifest 无效\u003C\u002Ftd>\n\u003Ctd>DevTools → Application → Manifest\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>图标 404\u003C\u002Ftd>\n\u003Ctd>curl 验证图标路径\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Service Worker 未激活\u003C\u002Ftd>\n\u003Ctd>DevTools → Service Workers\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>事件监听太晚\u003C\u002Ftd>\n\u003Ctd>移到最早的 JS\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>用户参与度不足\u003C\u002Ftd>\n\u003Ctd>启用 bypass flag 测试\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>已经安装过\u003C\u002Ftd>\n\u003Ctd>检查 standalone 模式\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>iOS 设备\u003C\u002Ftd>\n\u003Ctd>专门处理，引导手动安装\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>按照这个清单逐一排查，基本上能解决 99% 的问题。PWA 的安装机制设计有些繁琐，但一旦理解了背后的逻辑，实现起来并不困难。\u003C\u002Fp>\n","2026-05-02",[11,12,13],"pwa","javascript","web",false,[16,29,40,52,62,69,76,83,90,97,107,116,126,135,143,151,160,169,178,181,188,198,204,211,217,226,233,240,248,258,267,276,286,296,306,314,324,335,345,354,362,368,376,384,392,400,408,415],{"slug":17,"title":18,"description":19,"pub_date":20,"tags":21,"draft":14,"word_count":28},"ide-skills-guide","Agent Skills 完全指南：21 款第三方 Skill 深度评测与使用心得","全面评测 21 款第三方 Agent Skills，涵盖 Vue 生态、前端设计、构建工具、实用工具四大分类。从安装配置到实际使用场景，带你了解每个 Skill 的功能特点、最佳实践与使用心得。","2026-06-15",[22,23,24,25,26,27],"agent","skills","AI","效率工具","前端","Vue",4169,{"slug":30,"title":31,"description":32,"pub_date":33,"tags":34,"draft":14,"word_count":39},"linux-kernel-skeleton-struct-funcptr-container_of","Linux 内核骨架：struct、函数指针与 container_of","读懂 Linux 内核源码的三件套：巨大的 struct 组合代替继承、函数指针表实现虚派发、container_of 宏从嵌入成员找回完整对象。","2026-05-09",[35,36,37,38],"linux","kernel","C","container_of",1369,{"slug":41,"title":42,"description":43,"pub_date":44,"tags":45,"draft":14,"word_count":51},"astro-complete-guide-2025","Astro 5 深度剖析：Islands 架构原理、构建优化与 Cloudflare Workers 边缘部署","从编译器视角解析 Astro 5 的 Islands 架构实现原理，Content Layer API 的 Vite 插件机制，Server Islands 的流式渲染，以及如何在 Cloudflare Workers + D1 边缘环境下榨干性能。","2026-05-08",[46,47,48,49,50],"astro","frontend","cloudflare","performance","architecture",3663,{"slug":53,"title":54,"description":55,"pub_date":56,"tags":57,"draft":14,"word_count":61},"llm-prompt-engineering","Prompt Engineering 实战：让 LLM 真正听话的技巧","System prompt 怎么写、Few-shot 怎么设计、Chain-of-Thought 原理，以及常见失败模式和调试方法。","2026-05-03",[58,59,60],"ai","llm","工程实践",1723,{"slug":63,"title":64,"description":65,"pub_date":56,"tags":66,"draft":14,"word_count":68},"rag-system-design","RAG 系统设计：从 naive 到 production-ready","Retrieval-Augmented Generation 不只是「向量数据库 + LLM」，分块策略、召回质量、重排序、缓存才是工程核心。",[58,67,59,60],"rag",1613,{"slug":70,"title":71,"description":72,"pub_date":56,"tags":73,"draft":14,"word_count":75},"git-advanced-workflow","Git 进阶工作流：rebase、cherry-pick、bisect 的正确使用","merge 会了，但 rebase 总搞错？bisect 找 bug 提交？interactive rebase 整理历史？这篇一次说清楚。",[74,60],"git",1396,{"slug":77,"title":78,"description":79,"pub_date":56,"tags":80,"draft":14,"word_count":82},"docker-practical-guide","Docker 实战：从会用到用好","会 docker run 不够，Dockerfile 最佳实践、多阶段构建、Compose 编排、镜像瘦身才是日常真正需要的。",[81,35,60],"docker",1268,{"slug":84,"title":85,"description":86,"pub_date":56,"tags":87,"draft":14,"word_count":89},"anthropics-skills-guide","anthropics\u002Fskills：Anthropic 官方 Agent Skills 仓库解析","Anthropic 官方开源的 Agent Skills 标准仓库，127k stars，解析 SKILL.md 规范、17 个示例 skill 的设计模式，以及如何在 Claude Code \u002F Claude.ai \u002F API 中使用",[58,88,22,23],"Claude",2090,{"slug":91,"title":92,"description":93,"pub_date":56,"tags":94,"draft":14,"word_count":96},"karpathy-claude-code-guidelines","Karpathy 的 LLM 编码批评与 CLAUDE.md 最佳实践","基于 Andrej Karpathy 对 LLM 编程助手的观察，forrestchang 提炼出一个 CLAUDE.md 文件，4 条原则解决 AI 编码的典型失控问题：乱猜假设、过度设计、乱改代码、目标不清",[58,88,95,60],"Claude Code",2699,{"slug":98,"title":99,"description":100,"pub_date":56,"tags":101,"draft":14,"word_count":106},"typescript-advanced-patterns","TypeScript 高级模式：让类型系统为你工作","基础 TS 会了但类型总是 any？条件类型、映射类型、模板字面量类型、infer 关键字才是 TS 的真正威力。",[102,103,104,105],"typescript","类型系统","前端工程","高级模式",1419,{"slug":108,"title":109,"description":110,"pub_date":56,"tags":111,"draft":14,"word_count":115},"linux-performance-tuning","Linux 性能调优实战：从 top 到 perf 的完整工具链","遇到性能问题不知道从哪下手？这篇建立系统化的排查思路，从 CPU\u002F内存\u002FIO\u002F网络逐层分析。",[35,112,113,114],"性能","运维","系统编程",1524,{"slug":117,"title":118,"description":119,"pub_date":56,"tags":120,"draft":14,"word_count":125},"python-functional-programming","Python 函数式编程：map\u002Ffilter\u002Freduce 之外","Python 不是纯函数式语言，但 functools、itertools、偏函数、闭包这些工具用好了能让代码简洁一个量级。",[121,122,123,124],"python","函数式","闭包","装饰器",1867,{"slug":127,"title":128,"description":129,"pub_date":56,"tags":130,"draft":14,"word_count":134},"python-oop-guide","Python 面向对象：__init__ 之外你需要知道的","Python OOP 不只是 class + __init__，魔术方法、描述符、元类才是真正的武器。",[121,131,132,133],"OOP","面向对象","魔术方法",1792,{"slug":136,"title":137,"description":138,"pub_date":56,"tags":139,"draft":14,"word_count":142},"python-data-structures","Python 内置数据结构深度解析","list、dict、set、tuple 不只是数据容器，搞懂它们的底层实现和时间复杂度，才能写出高性能 Python。",[121,140,112,141],"数据结构","算法",1517,{"slug":144,"title":145,"description":146,"pub_date":56,"tags":147,"draft":14,"word_count":150},"python-basics-quick-start","Python 快速上手：写给有编程基础的人","已经会其他语言，想快速掌握 Python 的语法特性和思维方式，这篇是捷径。",[121,148,149],"入门","基础",1607,{"slug":152,"title":153,"description":154,"pub_date":56,"tags":155,"draft":14,"word_count":159},"python-dataclass-pydantic","Python dataclass vs Pydantic：数据类选型指南","dataclass 是标准库的轻量选择，Pydantic v2 是带验证的重武器，什么时候用哪个，这篇说清楚。",[121,156,157,158],"dataclass","pydantic","数据验证",1323,{"slug":161,"title":162,"description":163,"pub_date":56,"tags":164,"draft":14,"word_count":168},"python-asyncio-practical","Python asyncio 实战：从回调地狱到协程优雅","asyncio 是 Python 异步编程的核心，搞懂 event loop、Task、gather 这些概念才能写出真正高效的异步代码。",[121,165,166,167],"asyncio","并发","网络编程",1258,{"slug":170,"title":171,"description":172,"pub_date":56,"tags":173,"draft":14,"word_count":177},"python-type-hints-guide","Python 类型注解完全指南：从入门到实践","Python 3.5+ 引入类型注解，配合 mypy\u002Fpyright 让 Python 也能享受静态类型检查的好处。",[121,174,175,176],"typescript-style","type-hints","工具链",1102,{"slug":4,"title":5,"description":6,"pub_date":9,"tags":179,"draft":14,"word_count":180},[11,12,13],1683,{"slug":182,"title":183,"description":184,"pub_date":185,"tags":186,"draft":14,"word_count":187},"openclaw-vs-hermes-agent","OpenClaw vs Hermes Agent：两个本地优先 Agent 的设计差异","OpenClaw（Novita AI）和 Hermes Agent（Nous Research）都是本地运行的个人 AI Agent，但在记忆系统、技能学习、运行环境和模型生态上走了不同的路。深入对比两种架构的核心差异。","2026-05-01",[58,22,59],1679,{"slug":189,"title":190,"description":191,"pub_date":185,"tags":192,"draft":14,"word_count":197},"cpp-random-design-patterns","C++ 设计模式实战：RAII、观察者、工厂","用现代 C++（C++17\u002F20）实现三种高频设计模式：RAII 资源管理、观察者模式事件系统、工厂模式插件架构。每种模式给出问题场景、实现代码和真实工程案例。",[193,194,195,196],"cpp","设计模式","c++17","工程",2613,{"slug":199,"title":200,"description":201,"pub_date":185,"tags":202,"draft":14,"word_count":203},"data-structures-fundamentals","数据结构基础：从数组到红黑树","系统梳理常用数据结构的核心原理、时间复杂度和适用场景。数组、链表、栈、队列、哈希表、二叉树、堆、图，每种结构附实现要点和 C++ 代码片段。",[140,141,193,149],3004,{"slug":205,"title":206,"description":207,"pub_date":208,"tags":209,"draft":14,"word_count":210},"ai-agent-what-is","什么是 AI Agent？从 LLM 到自主执行","LLM 本身是无状态问答机，Agent 是什么让它’动’起来的？本文深入解析 Agent 的四个核心能力、ReAct 框架、工具调用原理，以及主流框架横向对比。","2026-04-30",[58,22,59],2116,{"slug":212,"title":213,"description":214,"pub_date":208,"tags":215,"draft":14,"word_count":216},"ai-agent-memory","AI Agent 的记忆系统：从上下文窗口到长期记忆","深入拆解 AI Agent 的四种记忆类型、上下文窗口压缩策略、RAG 向量检索原理，以及三种典型失败模式和工程选型建议。",[58,22,67],2052,{"slug":218,"title":219,"description":220,"pub_date":208,"tags":221,"draft":14,"word_count":225},"network-proxy-vpn-guide","代理与翻墙技术原理：从 HTTP 代理到现代协议","深入解析代理与 VPN 的本质区别，梳理从 SOCKS5 到 Shadowsocks、V2Ray\u002FXray、Hysteria2 的协议演进，以及机场订阅的技术本质。",[222,223,224],"网络","代理","协议",2148,{"slug":227,"title":228,"description":229,"pub_date":208,"tags":230,"draft":14,"word_count":150},"algorithm-binary-search","二分查找：永远写不对？记住这个模板","彻底搞清楚二分查找的边界问题：闭区间和左闭右开两套模板、三道经典 LeetCode 题目完整 C++ 实现，以及二分答案的进阶思路。",[141,231,232,193],"二分查找","leetcode",{"slug":234,"title":235,"description":236,"pub_date":208,"tags":237,"draft":14,"word_count":239},"algorithm-sliding-window","滑动窗口算法：从暴力到 O(n) 的思维跃迁","系统讲解滑动窗口算法的核心模板、适用题型，配合三道经典 LeetCode 题目的完整 C++ 实现，彻底理解双指针收缩思路。",[141,238,232,193],"滑动窗口",1943,{"slug":241,"title":242,"description":243,"pub_date":208,"tags":244,"draft":14,"word_count":247},"network-clash-config","Clash \u002F Mihomo 配置详解：规则、策略组与分流","深入解析 Clash\u002FMihomo 的核心配置结构，包括代理节点、策略组类型、规则优先级、DNS fake-ip 模式，以及一份实用的完整配置模板。",[222,245,223,246],"clash","配置",1292,{"slug":249,"title":250,"description":251,"pub_date":252,"tags":253,"draft":14,"word_count":257},"hid-hotplug","HID 设备热插拔检测：从 udev 到 node-hid","在 Linux 上用 node-hid + usb 库实现可靠的 USB HID 设备热插拔检测，踩坑记录","2026-04-28",[193,254,35,255,256],"hid","nodejs","electron",2039,{"slug":259,"title":260,"description":261,"pub_date":262,"tags":263,"draft":14,"word_count":266},"electron-ipc-types","Electron IPC 类型安全：从 any 到完全类型化","用 TypeScript 泛型封装 Electron IPC，彻底消灭 any，preload 契约集中管理","2026-04-25",[256,102,264,265],"ipc","vue",1446,{"slug":268,"title":269,"description":270,"pub_date":271,"tags":272,"draft":14,"word_count":275},"element-plus-popover-hide","手动关闭多个 el-popover（不用 v-model:visible）","通过 ref + Reflect.get 调用 hide() 方法手动关闭 Element Plus Popover，解释 Vue3 Proxy 导致无法直接调用实例方法的原因。","2024-10-25",[265,273,274],"element-plus","vue3",1321,{"slug":277,"title":278,"description":279,"pub_date":280,"tags":281,"draft":14,"word_count":285},"vite-vue3-ts-elementplus-pinia","用 Vite+（vp）从零搭建 Vue3 + TypeScript + Element Plus + Pinia + Vue Router","使用 Vite+ 统一工具链（vp）一条命令搭建 Vue3 全家桶，涵盖按需导入、Pinia store、路由配置，以及常见坑的解决方案。","2024-08-27",[265,282,102,273,283,284],"vite","pinia","vite-plus",1960,{"slug":287,"title":288,"description":289,"pub_date":290,"tags":291,"draft":14,"word_count":295},"cef-lnk2038-iterator-debug-level","CEF LNK2038：解决 _ITERATOR_DEBUG_LEVEL 不匹配错误","分析 CEF（Chromium Embedded Framework）集成时出现的 LNK2038 _ITERATOR_DEBUG_LEVEL 链接错误，从根本原因到解决方案的完整指南。","2024-05-07",[193,292,293,294],"CEF","Visual Studio","链接错误",1509,{"slug":297,"title":298,"description":299,"pub_date":300,"tags":301,"draft":14,"word_count":305},"npm-electron-install-fix","彻底解决 npm 安装 Electron 失败的问题","分析 npm install electron 失败的根本原因（下载二进制超时\u002F被墙），通过国内镜像（npmmirror）彻底解决，并介绍多种备选方案和常见错误排查。","2024-03-01",[256,302,303,304],"npm","前端工具链","国内镜像",1494,{"slug":307,"title":308,"description":309,"pub_date":310,"tags":311,"draft":14,"word_count":313},"git-out-of-memory","解决 git 报错：Fatal: Out of memory, malloc failed","分析 git 大仓库操作时出现 Out of memory malloc failed 的根本原因，通过调整 pack.windowMemory、http.postBuffer 和 git repack 彻底解决。","2024-01-31",[74,35,312],"工具",2244,{"slug":315,"title":316,"description":317,"pub_date":318,"tags":319,"draft":14,"word_count":323},"vmware-tools-install","在 VMware 虚拟机中安装 open-vm-tools 完整指南","详解 VMware Tools 的作用、open-vm-tools 与官方 VMware Tools 的区别，以及在 Ubuntu 虚拟机中安装并生效的完整步骤和常见问题排查。","2023-11-21",[320,35,321,322],"VMware","Ubuntu","虚拟机",2523,{"slug":325,"title":326,"description":327,"pub_date":328,"tags":329,"draft":14,"word_count":334},"load-balancing-algorithms","负载均衡算法完全指南：从轮询到一致性哈希","系统梳理静态与动态负载均衡算法，涵盖轮询、随机、权重、IP Hash、一致性 Hash、最少连接、最快响应等，并对比 Nginx、Dubbo、Spring Cloud LoadBalancer 的实现差异。","2023-11-15",[330,331,332,333],"分布式","负载均衡","Nginx","微服务",1764,{"slug":336,"title":337,"description":338,"pub_date":339,"tags":340,"draft":14,"word_count":344},"win-cw2a-ca2w","ATL 字符串转换：CW2A 与 CA2W 完全指南","详解 ATL 宏 CW2A\u002FCA2W 在 Unicode 与 ANSI 之间的字符串转换用法、头文件依赖、USES_CONVERSION 宏的作用与常见陷阱。","2023-06-09",[193,341,342,343],"windows","ATL","字符串",1665,{"slug":346,"title":347,"description":348,"pub_date":339,"tags":349,"draft":14,"word_count":353},"csharp-sendmessage-cpp","C# 通过 SendMessage 向 C++ 窗口发送消息与字符串","使用 P\u002FInvoke 调用 user32.dll 的 SendMessage，从 C# 发送自定义 WM_USER 消息及字符串指针给 C++ 原生窗口，并在 C++ 侧正确接收和转换。",[350,193,341,351,352],"C#","互操作","PInvoke",1554,{"slug":355,"title":356,"description":357,"pub_date":358,"tags":359,"draft":14,"word_count":361},"win-postmessage-vector","Windows PostMessage 跨线程传递 std::vector 指针","通过 PostMessage 在 Windows 消息队列中传递 std::vector 指针，使用 reinterpret_cast 将指针装入 LPARAM，并在接收方正确释放内存。","2023-05-26",[193,341,360],"WinAPI",1823,{"slug":363,"title":364,"description":365,"pub_date":358,"tags":366,"draft":14,"word_count":367},"exe-dll-single-package","将 EXE 和 DLL 打包成单一可执行文件","介绍两种将 exe 和依赖 dll 打包成单文件的方案：Enigma Virtual Box 和 WinRAR 自解压，适合发布 Windows 桌面程序时简化分发流程。",[341,193,312],1619,{"slug":369,"title":370,"description":371,"pub_date":358,"tags":372,"draft":14,"word_count":375},"cpp-random-mt19937","C++ 现代随机数生成：用 mt19937 彻底告别 rand()","深入讲解为什么 rand() 不够用，以及如何用 C++11 的 \u003Crandom> 库正确生成高质量随机数，涵盖 mt19937、各种分布和线程安全。",[193,373,374],"c++11","random",1549,{"slug":377,"title":378,"description":379,"pub_date":380,"tags":381,"draft":14,"word_count":383},"win-startup-registry","C++ 实现程序开机自启动：注册表方式详解","通过操作 Windows 注册表 Run 键实现程序开机自启动，包括 HKCU 与 HKLM 区别、完整封装代码、工作目录问题和 UAC 权限处理。","2022-12-26",[341,193,382],"registry",1201,{"slug":385,"title":386,"description":387,"pub_date":388,"tags":389,"draft":14,"word_count":391},"mfc-cstring-wparam","MFC 中 CString 与 WPARAM 之间的转换","详解 MFC 消息传递中 CString 无法直接强转为 WPARAM 的原因，以及两种正确的转换方案，并介绍结构体指针传递的正确姿势。","2022-11-25",[390,193,341],"mfc",1546,{"slug":393,"title":394,"description":395,"pub_date":396,"tags":397,"draft":14,"word_count":399},"duilib-static-build","正确编译 Duilib 静态库：避免 ATL 依赖和链接错误","详解如何用 DuiLib_Static.vcxproj 编译 Duilib 静态库，解决 VARIANT 未定义、Unicode 配置不匹配和 ATL 依赖等常见问题。","2022-08-24",[193,398,341,390],"duilib",2639,{"slug":401,"title":402,"description":403,"pub_date":404,"tags":405,"draft":14,"word_count":407},"mfc-dpi-adaptive","MFC 界面自适应不同分辨率","MFC 对话框程序实现控件和字体随分辨率自动缩放的完整方案，附 DPI Awareness 配置说明","2022-08-17",[390,193,341,406],"dpi",1414,{"slug":409,"title":410,"description":411,"pub_date":412,"tags":413,"draft":14,"word_count":414},"mfc-drag-window","MFC 无标题栏窗口客户区拖动：三种方法对比","MFC 对话框去掉标题栏后如何实现拖动移动窗口，三种方案完整实现与适用场景分析","2022-08-16",[390,193,341],1633,{"slug":416,"title":417,"description":418,"pub_date":419,"tags":420,"draft":14,"word_count":422},"algorithm-number-complement","整数的补数：位运算掩码解法","LeetCode 476 题，用掩码 XOR 实现整数补数，附 C++\u002FPython\u002FJava 三种实现及补数与补码的区别","2021-03-08",[141,421,232],"位运算",1374,[]]