[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"home-posts":3,"home-short-posts":112},[4,19,32,48,59,75,88,101],{"id":5,"slug":6,"title":7,"description":8,"content":9,"date":10,"image":11,"imageAlt":7,"author":12,"categories":14,"path":17,"minRead":18},3904,"checkmarx-client-dynamic-file-inclusion","[筆記] Checkmarx 中遇到 Client Dynamic File Inclusion 問題","前端專案中遇到 Client Dynamic File Inclusion 的高風險回報,出問題的地方是在這樣 […]","\u003Cp>前端專案中遇到 Client Dynamic File Inclusion 的高風險回報,出問題的地方是在這樣的地方,舉例來說是一個表單詢問:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">async submitForm(name){\n\n  const sendData = new FormData()\n  formData.append('name', name)  \u002F\u002F 出問題\n  const response = await axios.post(\n    API_CONFIG.baseURL + '&lt;api endpoint&gt;',\n    sendData,\n    {\n      headers: {\n        'Content-Type': 'application\u002Fx-www-form-urlencoded'\n      }\n    }\n  )\n\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>其中的 \u003Ccode>append\u003C\u002Fcode> 被認為有 Client Dynamic File Inclusion 風險:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">formData.append('name', name)\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>先來看看他的說明\u003C\u002Fh2>\n\u003Ch3>可能發生什麼問題\u003C\u002Fh3>\n\u003Cblockquote>\n\u003Cp>If an attacker can select the name of the library, or the location of the code file that is loaded by the application, they would be able to cause the application to execute arbitrary code. This effectively allows the attacker to control the code run by the application. Execution of code in web application context may result with Cross-Site Scripting.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>可能發生的問題是:如果攻擊者可以選擇應用程式要載入的程式庫名稱或程式碼檔案位置,他們就能讓應用程式執行任意的惡意程式碼。這等於讓攻擊者能控制應用程式執行的內容。在網頁應用程式的執行環境中,這種情況可能導致跨網站指令碼攻擊(XSS, Cross-Site Scripting)。\u003C\u002Fp>\n\u003Ch3>如何發生\u003C\u002Fh3>\n\u003Cblockquote>\n\u003Cp>The application uses untrusted data to specify the library or code file, without proper sanitization. This causes the application to load any arbitrary code, as specified. The loaded code will then be executed.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>發生的情況是:應用程式在指定要載入的程式庫或程式碼檔案時,使用了未受信任的資料(例如來自使用者的輸入),而且沒有進行適當的驗證或過濾。結果導致應用程式可能載入並執行任何由攻擊者指定的惡意程式碼。\u003C\u002Fp>\n\u003Ch3>如何避免\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>不要動態載入程式庫或程式碼檔案,特別是不要根據使用者輸入動態載入。\u003C\u002Fli>\n\u003Cli>如果確實有必要根據未受信任的資料來選擇要載入的程式庫,應該驗證該資料是否屬於預先定義的白名單(whitelist)中的合法項目。或者,將使用者輸入作為「識別碼(identifier)」來對應選擇白名單內的安全程式庫。\u003C\u002Fli>\n\u003Cli>對所有未受信任的資料進行驗證與完整性檢查(integrity check),確保載入或處理的程式庫、檔案是可信任且未被竄改的。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>然後有一些範例如下:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">&lt;script&gt;\nconst queryString = window.location.search;\nconst urlParams = new URLSearchParams(queryString);\nconst lib = urlParams.get('lib');\nvar script = document.createElement(\"script\");\nconst safeDomain = 'https:\u002F\u002F' + TRUST_DOMAIN_NAME + '\u002F';\nscript.src = safeDomain + lib;\nvar element = document.getElementById(DIV_ID);\nelement.appendChild(script);\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在 Stack Overflow 中可以找到類似的討論:\u003Cbr \u002F>\n\u003Ca href=\"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F76356081\u002Fhow-to-mitigate-against-client-dynamic-file-inclusion-vulnerability-detected-by\" target=\"_blank\" rel=\"noopener\">How to mitigate against Client Dynamic File Inclusion vulnerability detected by Checkmarx\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>心得\u003C\u002Fh2>\n\u003Cp>說明滿清楚的,但是很納悶為什麼是 \u003Ccode>append\u003C\u002Fcode> 出問題,合理懷疑是因為 Checkmarx 認為這邊的 \u003Ccode>append\u003C\u002Fcode> 跟 jQuery 的 \u003Ccode>append\u003C\u002Fcode> 是一樣的,但我的理解是 \u003Ccode>formData.append()\u003C\u002Fcode> 不會直接執行內容,不會造成動態載入的狀況。\u003C\u002Fp>\n\u003Cp>不過目前的解決方式就是不要用 \u003Ccode>append\u003C\u002Fcode> 來處理 formData,而是用一次性賦值的做法來打包 formData。\u003C\u002Fp>\n","2026-08-12T18:10:18","",{"name":13,"link":11},"橫隔膜",[15],{"name":16,"slug":16},"frontend","\u002Fblog\u002Fcheckmarx-client-dynamic-file-inclusion",2,{"id":20,"slug":21,"title":22,"description":23,"content":24,"date":25,"image":11,"imageAlt":22,"author":26,"categories":27,"path":31,"minRead":18},3903,"筆記-claude-code-discord-串接設定紀錄","[筆記] Claude Code Discord 串接設定紀錄","記錄 Claude Code 透過 Discord 插件與 Discord 頻道串接的完整流程。設定日期：20 […]","\u003Cblockquote>\n\u003Cp>記錄 Claude Code 透過 Discord 插件與 Discord 頻道串接的完整流程。\u003Cbr \u002F>設定日期：2026-04-02\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2>概述\u003C\u002Fh2>\n\u003Cp>Claude Code 支援透過 Discord 插件（需要 \u003Cstrong>Bun\u003C\u002Fstrong> 環境），讓使用者在 Discord 頻道中直接與 Claude Code session 對話。Bot 收到訊息後會轉發到 Claude Code，回覆也會透過 Bot 發送回 Discord。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>限制：\u003C\u002Fstrong>一個 Bot Token 同時只能有一個活躍的 Claude Code session 連線。\u003C\u002Fp>\n\u003Ch2>前置準備\u003C\u002Fh2>\n\u003Col>\n\u003Cli>前往 \u003Ca href=\"https:\u002F\u002Fdiscord.com\u002Fdevelopers\u002Fapplications\">Discord Developer Portal\u003C\u002Fa> 點選 \u003Cstrong>New Application\u003C\u002Fstrong> 並命名\u003C\u002Fli>\n\u003Cli>在 Bot 頁面建立 username，點選 \u003Cstrong>Reset Token\u003C\u002Fstrong> 並複製 Token\u003C\u002Fli>\n\u003Cli>開啟以下 Privileged Gateway Intents：\n\u003Cul>\n\u003Cli>\u003Cstrong>Message Content Intent\u003C\u002Fstrong>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>使用 OAuth2 &gt; URL Generator 產生邀請連結，勾選 \u003Ccode>bot\u003C\u002Fcode> scope，並給予以下權限：\n\u003Cul>\n\u003Cli>View Channels\u003C\u002Fli>\n\u003Cli>Send Messages\u003C\u002Fli>\n\u003Cli>Send Messages in Threads\u003C\u002Fli>\n\u003Cli>Read Message History\u003C\u002Fli>\n\u003Cli>Attach Files\u003C\u002Fli>\n\u003Cli>Add Reactions\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>開啟產生的 URL，將 Bot 邀請到你的 Discord 伺服器\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>設定步驟\u003C\u002Fh2>\n\u003Ch3>Step 1：安裝插件\u003C\u002Fh3>\n\u003Cp>在 Claude Code 中執行：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002Fplugin install discord@claude-plugins-official\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n\u003Cp>若提示找不到插件，先執行：\u003Cbr \u002F>\u003Ccode>\u002Fplugin marketplace update claude-plugins-official\u003C\u002Fcode>\u003Cbr \u002F>或若尚未新增：\u003Ccode>\u002Fplugin marketplace add anthropics\u002Fclaude-plugins-official\u003C\u002Fcode>\u003Cbr \u002F>再重新安裝。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>安裝後執行 \u003Ccode>\u002Freload-plugins\u003C\u002Fcode> 啟用插件的 configure 指令。\u003C\u002Fp>\n\u003Ch3>Step 2：設定 Bot Token\u003C\u002Fh3>\n\u003Cp>執行 configure 指令並帶入先前複製的 Token：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002Fdiscord:configure &lt;token&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Token 會被儲存到 \u003Ccode>~\u002F.claude\u002Fchannels\u002Fdiscord\u002F.env\u003C\u002Fcode>。也可以在啟動 Claude Code 前，在 shell 環境中設定 \u003Ccode>DISCORD_BOT_TOKEN\u003C\u002Fcode> 變數。\u003C\u002Fp>\n\u003Ch3>Step 3：以 channels 模式重新啟動\u003C\u002Fh3>\n\u003Cp>退出 Claude Code，並以 \u003Ccode>--channels\u003C\u002Fcode> 參數重新啟動，讓 Discord 插件建立連線：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>claude --channels plugin:discord@claude-plugins-official\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Step 4：配對帳號（DM Pairing）\u003C\u002Fh3>\n\u003Cp>在 Discord 上傳送 DM（私訊）給你的 Bot，Bot 會回傳一組配對碼。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>若 Bot 沒有回應，請確認 Claude Code 是否有以 \u003Ccode>--channels\u003C\u002Fcode> 啟動。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>回到 Claude Code，執行：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002Fdiscord:access pair &lt;code&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>鎖定存取權限，讓只有你的帳號可以觸發：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002Fdiscord:access policy allowlist\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Step 5：設定伺服器頻道（可選）\u003C\u002Fh3>\n\u003Cp>若要使用伺服器頻道而非 DM：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002Fdiscord:access group add &lt;頻道ID&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>預設 \u003Ccode>requireMention: true\u003C\u002Fcode>（需要 @Bot 才觸發），加 \u003Ccode>--no-mention\u003C\u002Fcode> 可關閉：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002Fdiscord:access group add &lt;頻道ID&gt; --no-mention\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>取得頻道 ID：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n\u003Cli>Discord 設定 → 進階 → 開啟「開發者模式」\u003C\u002Fli>\n\u003Cli>對文字頻道右鍵 → 「複製頻道 ID」\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>access.json 結構說明\u003C\u002Fh2>\n\u003Cp>檔案位置：\u003Ccode>~\u002F.claude\u002Fchannels\u002Fdiscord\u002Faccess.json\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{\n  \"dmPolicy\": \"allowlist\",\n  \"allowFrom\": [\"使用者ID\"],\n  \"groups\": {\n    \"頻道ID\": {\n      \"requireMention\": false,\n      \"allowFrom\": []\n    }\n  },\n  \"pending\": {}\n}\u003C\u002Fcode>\u003C\u002Fpre>\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>\u003Ccode>dmPolicy\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>DM 政策：\u003Ccode>pairing\u003C\u002Fcode>（配對碼）\u002F \u003Ccode>allowlist\u003C\u002Fcode>（白名單）\u002F \u003Ccode>disabled\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>allowFrom\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>允許 DM 的使用者 ID 列表\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>groups\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>伺服器頻道設定，key 為頻道 ID\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>groups.requireMention\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>true\u003C\u002Fcode>=需要 @Bot 才回應，\u003Ccode>false\u003C\u002Fcode>=所有訊息都回應\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>groups.allowFrom\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>該頻道允許觸發的使用者 ID，空陣列=所有人都可以\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>pending\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>等待配對確認的請求\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch2>已知問題\u003C\u002Fh2>\n\u003Ch3>DM 頻道無法回覆\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>現象：\u003C\u002Fstrong>Bot 可以收到 DM 訊息，但無法回覆，錯誤訊息為 \u003Ccode>channel is not allowlisted\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>原因：\u003C\u002Fstrong>Discord 插件的 outbound gate 使用 \u003Ccode>ch.recipientId\u003C\u002Fcode> 檢查 DM 頻道權限，但 Discord.js 的 partial channel 機制可能導致此值為 \u003Ccode>undefined\u003C\u002Fcode>，使得權限檢查失敗。Inbound gate 則使用 \u003Ccode>msg.author.id\u003C\u002Fcode>，所以收訊正常。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>解法：\u003C\u002Fstrong>使用伺服器（Guild）頻道而非 DM 頻道。伺服器頻道透過 \u003Ccode>groups\u003C\u002Fcode> 設定檢查，不受此問題影響。\u003C\u002Fp>\n\u003Ch2>常用指令參考\u003C\u002Fh2>\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>\u003Ccode>\u002Fplugin install discord@claude-plugins-official\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>安裝 Discord 插件\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:configure &lt;token&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>設定 Bot Token\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:access\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>查看目前存取狀態\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:access pair &lt;code&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>確認配對碼\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:access policy &lt;mode&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>設定 DM 政策（pairing\u002Fallowlist\u002Fdisabled）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:access group add &lt;頻道ID&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>新增伺服器頻道\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:access group add &lt;頻道ID&gt; --no-mention\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>新增頻道（不需 @Bot）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:access group rm &lt;頻道ID&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>移除頻道\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:access allow &lt;使用者ID&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>新增 DM 白名單\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:access remove &lt;使用者ID&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>移除 DM 白名單\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>\u002Fdiscord:access set requireMention false\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>關閉 @Bot 需求\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n","2026-08-12T00:15:24",{"name":13,"link":11},[28],{"name":29,"slug":30},"AI","ai","\u002Fblog\u002F筆記-claude-code-discord-串接設定紀錄",{"id":33,"slug":34,"title":35,"description":36,"content":37,"date":38,"image":11,"imageAlt":35,"author":39,"categories":42,"path":46,"minRead":47},3825,"radio-test","三等無線電考取經驗分享","就在2026年初，成功考到三等無線電執照啦～ 由於前陣子接觸了 Meshtastic 後，對無線電有些興趣，加 […]","\u003Cp>就在2026年初，成功考到三等無線電執照啦～\u003C\u002Fp>\n\u003Cp>由於前陣子接觸了 Meshtastic 後，對無線電有些興趣，加上時事關係，想說家裡備一台無線電也不錯，就開始準備相關知識，這時才知道如果要持有業餘無線電，就必須要考取執照。\u003C\u002Fp>\n\u003Cp>以下僅為個人濃縮的資訊，詳細資料請另外參考相關法規喔！\u003C\u002Fp>\n\u003Ch2>無線電分兩種\u003C\u002Fh2>\n\u003Col>\n\u003Cli>業務無線電 -&gt; 無需執照，通訊距離短\u003C\u002Fli>\n\u003Cli>業餘無線電 -&gt; 需要執照，且有三種等級，通訊距離較業務無線電長很多\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>合法持有業餘無線電流程\u003C\u002Fh2>\n\u003Col>\n\u003Cli>如果是無執照人員，需要先考到三等無線電執照\u003C\u002Fli>\n\u003Cli>購入一台合規的無線電\u003C\u002Fli>\n\u003Cli>搭配三等無線電執照申請電台執照，取得台呼後就正式合法持有業餘無線電\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>無線電考題可以在 NCC 的網站中找到，也有線上題庫可以練習。三等考題固定會考 35 題，只要答對 25 題就算合格。\u003C\u002Fp>\n\u003Cp>之前看到有人分享背題可以用閃卡記憶系統，我也按照這個方式進行，也因此自己做了一個相關工具ＸＤ\u003Cbr \u002F>\n\u003Ca href=\"https:\u002F\u002Fradio-test.billxu.net\u002F\">https:\u002F\u002Fradio-test.billxu.net\u002F\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>考照與申辦流程\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>週一和週五是隨報隨考，只要去考場就能抽好碼牌直接考試，要注意考試開放時間。\u003C\u002Fli>\n\u003Cli>當場可以直接用線上報名方式申請，因此不用特別印報名表\u003C\u002Fli>\n\u003Cli>報名完畢後，需要先繳交考試費，繳完費後就會叫你進考場考試。\u003C\u002Fli>\n\u003Cli>考場就是幾個辦公室隔間圍起來的電腦，考務人員會挑一台給你，坐下來開始答題，跟記憶中的考駕照很類似。\u003C\u002Fli>\n\u003Cli>考完就能馬上得到成績，確認合格後就回到櫃檯直接申請資格執照，需要一寸的大頭照。需要執照費。\u003C\u002Fli>\n\u003Cli>現場就會直接拿到執照，整個考試-&gt;取得執照時間約 40 分鐘。\u003C\u002Fli>\n\u003Cli>拿到執照後就可以立刻辦理電台執照(如果有機子的話)。\u003C\u002Fli>\n\u003Cli>我是另外找一天去辦理，所以一樣抽號碼牌。\u003C\u002Fli>\n\u003Cli>需要攜帶資格執照並且填寫完申請電台的文件。\u003C\u002Fli>\n\u003Cli>填寫完後就會驗機，記得要關閉擴頻，由於是三等電台，允許頻率範圍是 144.0 MHz~146.0 MHz 頻段和 430.0 MHz~440.0 MHz 頻段。\u003C\u002Fli>\n\u003Cli>驗機的流程就是看無線電能不能超出這些頻段。\u003C\u002Fli>\n\u003Cli>驗完一切都沒問題後，就取得電台執照啦！上面也會有台呼。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>考試心得\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>我大概背了一個月，第一週直接看原題庫 PDF ，覺得很難背。\u003C\u002Fli>\n\u003Cli>後來做了記憶工具後，就按照分類開始念。\u003C\u002Fli>\n\u003Cli>無線電原理邊看邊翻一些影片、或者別人寫的無線電資料以及問 AI ，去理解基礎無線電原理，這段花比較多時間\u003C\u002Fli>\n\u003Cli>由於考題分六類：無線電法規、無線電原理、射頻干擾、無線電通訊、電磁技術、無線電防護。\u003C\u002Fli>\n\u003Cli>我每天進行約兩次的題庫演練，就是進行線上題庫，確認目前的記憶程度，然後再根據不同分類閱讀題庫約50題。\u003C\u002Fli>\n\u003Cli>根據線上題庫體感來說，出題會依照分類題數比例，舉例來說無線電法規有200多題，幾乎是總題數 40% ，那線上考題 35 題中就大概有 12~13題。\u003C\u002Fli>\n\u003Cli>根據上述策略，去分配必拿題數的分類就可以。\u003C\u002Fli>\n\u003Cli>我先背熟少題數的射頻干擾、電磁技術、無線電防護。\u003C\u002Fli>\n\u003Cli>無線電法規整體很像常識題，也是很好背的一個分類。\u003C\u002Fli>\n\u003Cli>剩下原理和通訊就是盡量理解與反覆閱讀。\u003C\u002Fli>\n\u003Cli>依照上面策略，經過兩週後再進行線上測試發現明顯進步，原本答對率 10 題，明顯開始到 20 題左右。\u003C\u002Fli>\n\u003Cli>差不多第三周開始就保持合格了。我又多背了一週，讓答題數到30題左右，避免考試當天失常。\u003C\u002Fli>\n\u003Cli>由於題庫不會更動，也就是選項完全一樣，如果真的背不起來答案，那就背答案位置XD，有幾題我就靠視覺記憶來處理，哈哈。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>總之，考試當天我錯了 3 題，算是最佳成績XDDD\u003Cbr \u002F>\n呼號為 BM3JMH 。\u003C\u002Fp>\n\u003Cp>大概就是醬，2026 年初就達到了原本不是目標的事，可喜可樂。\u003C\u002Fp>\n","2026-01-29T21:42:32",{"name":40,"link":41},"Chih Fan","http:\u002F\u002Fpost.billxu.net",[43],{"name":44,"slug":45},"生活","life","\u002Fblog\u002Fradio-test",1,{"id":49,"slug":50,"title":51,"description":52,"content":53,"date":54,"image":11,"imageAlt":51,"author":55,"categories":56,"path":58,"minRead":47},3822,"janat-grey-tea","意外發現很順口的伯爵茶 Janat 品牌","在咖樂迪發現一款很不錯的伯爵茶，叫做 Janat ，入手了格雷伯爵茶(Original Earl Grey)黃 […]","\u003Cp>在咖樂迪發現一款很不錯的伯爵茶，叫做 Janat ，入手了格雷伯爵茶(Original Earl Grey)黃盒(25包裝)，用大約 90 ~ 95 度泡 2 分鐘後拿掉茶包，佛手柑的香氣不會很強烈，喝起來非常順口。\u003Cbr \u002F>\n現在黑盒(50包裝)在特價，價格跟黃盒一樣，不太確定差異在哪？只知道是不同的產品系列，黑盒叫做 Heritage 。\u003C\u002Fp>\n\u003Cp>查了一下 Janat 似乎是法國品牌，Logo 有兩隻貓。水果茶似乎滿有名的，不過每次水果茶我都喝不久XD\u003C\u002Fp>\n\u003Cp>另外一提，加上15%比例的糖，然後塞爆冰塊，跟すき家的伯爵茶有87像，偶爾在家弄牛丼的時候可以高仿一下牛丼套餐。\u003C\u002Fp>\n","2026-01-20T18:31:04",{"name":40,"link":41},[57],{"name":44,"slug":45},"\u002Fblog\u002Fjanat-grey-tea",{"id":60,"slug":61,"title":62,"description":63,"content":64,"date":65,"image":11,"imageAlt":62,"author":66,"categories":67,"path":74,"minRead":47},3818,"個人網站改版建置心得-headless-wordpress-nuxt","個人網站改版建置心得 Headless WordPress + Nuxt","一直聽過 WordPress 的無頭模式(Headless)架構，最近心一狠就把自己的網站轉移成無頭模式架構。 […]","\u003Cp>一直聽過 WordPress 的無頭模式(Headless)架構，最近心一狠就把自己的網站轉移成無頭模式架構。\u003C\u002Fp>\n\u003Cp>理由有幾點：\u003C\u002Fp>\n\u003Col>\n\u003Cli>想要更有彈性的在前台開發\u003C\u002Fli>\n\u003Cli>在前台實作一些想法\u003C\u002Fli>\n\u003Cli>體驗看看無頭模式的坑(?\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>記錄一下目前版本做了哪些事情\u003C\u002Fp>\n\u003Ch1>轉換流程\u003C\u002Fh1>\n\u003Ch2>1. 決定架構\u003C\u002Fh2>\n\u003Cp>我決定了前台採用 Nuxt ，一方面我是走 Vue 生態的開發者，二來需要有 SSR 來維持原本的 SEO 效果。\u003C\u002Fp>\n\u003Cp>簡單來說就是 Nuxt + WordPress\u003C\u002Fp>\n\u003Ch2>2. 開啟 WordPress 無頭模式\u003C\u002Fh2>\n\u003Cp>翻了一些文章，多半會推薦無頭模式要採用 Graphql ，主要是為了提高查詢效率，因此先安裝了 wpgraphql ，讓 WordPress 的 RestFul API  支援 Graphql。\u003C\u002Fp>\n\u003Ch2>3. 建置新的前台\u003C\u002Fh2>\n\u003Cp>用 Nuxt 建置了新的前台，目前版本只是先提供最基礎的內容：首頁、介紹、文章等，後續還有一些想法慢慢更新。這時實作串接原網站的 API，就能進行測試了！\u003C\u002Fp>\n\u003Cp>記得要開啟 SSR 模式，這部分除錯比較麻煩一些。\u003C\u002Fp>\n\u003Cp>另外也要處理 CORS 問題，這部分解完後，整個架構雛形就完成了。\u003C\u002Fp>\n\u003Ch2>4. 網域轉換 + SEO\u003C\u002Fh2>\n\u003Cp>將原本的 WordPress 轉換到另一個網域，並且將新前台掛在原本的網域。\u003C\u002Fp>\n\u003Cp>由於新架構的 route 已經改變，自然就會出現文章路徑的轉址問題，我另外寫了一個轉換器和對應表來處理 301 轉址。\u003C\u002Fp>\n\u003Cp>而 SEO 就要在 Nuxt 上全部處理了。\u003C\u002Fp>\n\u003Ch2>5. 關閉 WordPress 的前台模式\u003C\u002Fh2>\n\u003Cp>說是關閉，我採用的做法是強制轉址到前台網域billxu.net 啦.\u003C\u002Fp>\n\u003Cp>這些步驟做完，也差不多把整個架構調完了！\u003C\u002Fp>\n\u003Ch1>簡短心得\u003C\u002Fh1>\n\u003Cp>轉換完後有種舒爽的感覺，想到不用受到原本頁面架構的限制就覺得有種解放感！不過什麼都得重新來過，像是文章排版、資料解析等等。最近還在除錯，之後想在網站中放一些小遊戲之類的(? 到時候就再分享了XD\u003C\u002Fp>\n","2026-01-18T15:29:46",{"name":40,"link":41},[68,71],{"name":69,"slug":70},"Nuxt","nuxt",{"name":72,"slug":73},"WordPress","wordpress","\u002Fblog\u002F個人網站改版建置心得-headless-wordpress-nuxt",{"id":76,"slug":77,"title":78,"description":79,"content":80,"date":81,"image":82,"imageAlt":78,"author":83,"categories":84,"path":87,"minRead":18},3786,"理解與前端實作-recaptcha-v3","理解與前端實作 reCAPTCHA v3","理解與實作 recaptcha Google reCAPTCHA 是一種常見的機器人驗證技術，用來區分人類使用 […]","\u003Ch1>理解與實作 recaptcha\u003C\u002Fh1>\n\u003Cp>Google reCAPTCHA  是一種常見的機器人驗證技術，用來區分人類使用者和自動化程式。現在主流有 v2 和 v3 兩種版本，是擁有免費額度的付費服務。\u003Cbr \u002F>\n先前都只有透過打包好的 WordPress 外掛來實作，這次有機會在Nuxt3的前端專案中自己寫，就趁機記錄一下自己的理解與過程。\u003C\u002Fp>\n\u003Cp>本次使用的是 reCAPTCHA v3 ，特點在於不用跟介面互動就能取得辨識結果，好處就是網站對使用者的驗證只會在背景處理，不影響使用體驗。\u003C\u002Fp>\n\u003Cp>reCAPTCHA v3的技術原理大概可以這樣理解：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>行為分析：評估使用者與頁面的互動方式，如滑鼠移動模式、點擊方式和速度。\u003C\u002Fli>\n\u003Cli>瀏覽器指紋識別：收集瀏覽器和設備資訊，如作業系統、瀏覽器類型、螢幕解析度等。\u003C\u002Fli>\n\u003Cli>Cookie 和歷史數據：分析用戶之前與 Google 服務的互動歷史。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>實作邏輯的話，其實就是跟 reCAPTCHA 服務取用資料這樣。\u003C\u002Fp>\n\u003Ch3>前置：取得金鑰\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>網站金鑰 (Site Key)：公開的，嵌入在網頁前端代碼中\u003C\u002Fli>\n\u003Cli>密鑰 (Secret Key)：私密的，只在伺服器端使用\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>實作流程\u003C\u002Fh3>\n\u003Cp>以下是我對  reCAPTCHA 溝通的理解：\u003C\u002Fp>\n\u003Ch4>1. 前端向 Google reCAPTCHA 請求 token\u003C\u002Fh4>\n\u003Cp>首先註冊並申請 Google reCAPTCHA 服務。\u003Cbr \u002F>\n網站前端載入 reCAPTCHA SDK，當使用者執行需驗證的操作時，呼叫grecaptcha.execute()， reCAPTCHA 服務會分析使用者行為後回傳 token。\u003C\u002Fp>\n\u003Ch4>2. 前端向網站伺服器提交請求\u003C\u002Fh4>\n\u003Cp>前端將 reCAPTCHA token 連同表單數據或 API 請求一起發送給後端。\u003C\u002Fp>\n\u003Ch4>3. 伺服器向 Google reCAPTCHA 驗證 token\u003C\u002Fh4>\n\u003Cp>後端使用密鑰(secret key)向 Google reCAPTCHA API 發送驗證請求。\u003C\u002Fp>\n\u003Ch4>4. Google reCAPTCHA 返回驗證結果\u003C\u002Fh4>\n\u003Cp>JSON 格式的回應包含:\u003Cbr \u002F>\nsuccess: 驗證是否成功(true\u002Ffalse)\u003Cbr \u002F>\nscore: 闕值，人類可能性分數(0.0-1.0)\u003Cbr \u002F>\naction: 觸發驗證的行為名稱\u003Cbr \u002F>\nchallenge_ts: 時間戳記\u003Cbr \u002F>\nhostname: 網站域名\u003C\u002Fp>\n\u003Ch3>閾值判斷：\u003C\u002Fh3>\n\u003Cp>我這次使用的 reCAPTCHA v3 會回傳 0.0 到 1.0 之間的分數，此分數表示使用者行為的「人類可能性」，管理者可自行決定接受的閾值（例如 0.5）高於閾值就允許後續操作，低於則可能需要額外驗證或拒絕請求。\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fpost.billxu.net\u002Fwp-content\u002Fuploads\u002F2025\u002F04\u002Fdiagram.svg\">\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2025%2F04%2Fdiagram.svg\" alt=\"\" \u002F>\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>實作紀錄：\u003C\u002Fh2>\n\u003Cp>我使用 vue-recaptcha-v3 ，自行建立 plugin 管理與啟動 reCAPTCHA，因為只有前端實作，所以很快就處理好了。\u003C\u002Fp>\n\u003Cp>recaptcha.client.js\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { VueReCaptcha } from &#039;vue-recaptcha-v3&#039;\n\nexport default defineNuxtPlugin(async (nuxtApp) =&gt; {\n  const config = useRuntimeConfig()\n\n  \u002F\u002F 確保 siteKey 存在並輸出調試信息\n  console.log(&#039;Initializing reCAPTCHA with config:&#039;, {\n    siteKey: config.public.recaptchaSiteKey,\n    isDevelopment: process.env.NODE_ENV === &#039;development&#039;\n  })\n\n  if (!config.public.recaptchaSiteKey) {\n    console.error(&#039;reCAPTCHA site key is missing&#039;)\n    return\n  }\n\n  try {\n    await nuxtApp.vueApp.use(VueReCaptcha, {\n      siteKey: config.public.recaptchaSiteKey,\n      loaderOptions: {\n        useRecaptchaNet: true, \u002F\u002F 從 recaptcha.net 載入 script\n        autoHideBadge: false,\u002F\u002F 隱藏標籤\n        explicitRenderParameters: {\n          badge: &#039;bottomright&#039;,\n          size: &#039;invisible&#039;\n        }\n      }\n    })\n\n    \u002F\u002F 驗證 reCAPTCHA 是否正確載入\n    const recaptcha = await window.grecaptcha\n    if (!recaptcha) {\n      throw new Error(&#039;reCAPTCHA not loaded properly&#039;)\n    }\n\n    console.log(&#039;reCAPTCHA plugin initialized successfully&#039;)\n  } catch (error) {\n    console.error(&#039;Failed to initialize reCAPTCHA:&#039;, error)\n  }\n}) \u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然後在 .env 和 nuxt.config 放好 API_KEY\u003C\u002Fp>\n\u003Cp>例如在登入時就宣告上述的流程即可\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { useReCaptcha } from &#039;vue-recaptcha-v3&#039;\nconst { executeRecaptcha, recaptchaLoaded } = useReCaptcha()\n\nconst handleLogin = async (e) =&gt; {\n\n\u002F\u002F 執行 reCAPTCHA 驗證\nconsole.log(&#039;開始執行 reCAPTCHA 驗證&#039;)\nconst recaptchaToken = await executeRecaptcha(&#039;login&#039;)\nconsole.log(&#039;recaptchaToken:&#039;, recaptchaToken)\n\n\u002F\u002F 發送登入請求\nconst response = await $fetch(`\u002Fapi\u002Flogin`, {\n    method: &#039;POST&#039;,\n        body: {\n        account: account.value,\n        password: password.value,\n        recaptchaToken: recaptchaToken\n        }\n    })\nif (response) {\n\u002F\u002F後端回傳闕值做後續處理\n}\n\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>後端就要另外寫一隻配合密鑰的請求去詢問結果。\u003C\u002Fp>\n\u003Cp>成功載入的話，網站會出現 reCAPTCHA 的標籤，預設在右下方。\u003C\u002Fp>\n","2025-04-02T12:29:05","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F09%2FNuxt_logo.svg.png",{"name":40,"link":41},[85,86],{"name":16,"slug":16},{"name":69,"slug":70},"\u002Fblog\u002F理解與前端實作-recaptcha-v3",{"id":89,"slug":90,"title":91,"description":92,"content":93,"date":94,"image":95,"imageAlt":91,"author":96,"categories":97,"path":100,"minRead":47},3775,"nuxt3-vue-3-動態取得圖片","Nuxt3 \u002F Vue 3 動態取得圖片","Nuxt3 \u002F Vue 3 動態取得圖片 參考資料 https:\u002F\u002Fwww.lichter.io\u002Farticl […]","\u003Ch1>Nuxt3 \u002F Vue 3 動態取得圖片\u003C\u002Fh1>\n\u003Cp>參考資料\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fwww.lichter.io\u002Farticles\u002Fnuxt3-vue3-dynamic-images\u002Fhttps:\u002F\u002Fclairechang.tw\u002F2023\u002F09\u002F12\u002Fnuxt3\u002Fnuxt-v3-assets-vs-public\u002F\">https:\u002F\u002Fwww.lichter.io\u002Farticles\u002Fnuxt3-vue3-dynamic-images\u002Fhttps:\u002F\u002Fclairechang.tw\u002F2023\u002F09\u002F12\u002Fnuxt3\u002Fnuxt-v3-assets-vs-public\u002F\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>把圖片放在assets中後，通常來說會使用這兩種寫法\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img src=&quot;@\u002Fassets\u002Fimage\u002Flogo.jpg&quot;&gt; \n&lt;img src=&quot;..\u002Fassets\u002Fimage\u002Flogo.jpg&quot;&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但只限於靜態圖片，如果想要動態取用圖片。\u003Cbr \u002F>\n例如有10張圖片，檔名是1~10，例如001.jpg ~ 010.jpg\u003C\u002Fp>\n\u003Cp>以前我偷懶會這樣處理：\u003Cbr \u002F>\n放在 public 中，直接取用\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img src=&quot;`\u002Fimage\u002F${index}.jpg`&quot;&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但這種作法有些缺點，\u003C\u002Fp>\n\u003Cul>\n\u003Cli>圖片將被瀏覽器快取住，因此如果我們想要更改圖像，我們也必須更改檔案名稱。這不是最佳的，特別是如果我們想在多個地方使用相同的圖像。\u003C\u002Fli>\n\u003Cli>效能問題，圖片不會被Vite插件之類最佳化。\u003C\u002Fli>\n\u003Cli>圖片將以原始大小加載，如果尺寸很大，同樣也有瀏覽器效能問題。\u003C\u002Fli>\n\u003Cli>管理不一致，這是我的使用習慣，我不喜歡圖片要放在public裡面，比較偏好在assets中一起管理。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>如果換成assets中的話….\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img src=&quot;..\u002Fassets\u002Fimage\u002F${index}.jpg`&quot;&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>會失效。代表取用不到資源\u003C\u002Fp>\n\u003Cp>如果找解決方式可能會找到 Vue2 時代的作法，加入 \u003Cstrong>require，\u003C\u002Fstrong>例如\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img :src=&quot;require(`...\u002Fassets\u002Fimage\u002F${index}.jpg`)&quot; :alt=&quot;selectedDog&quot;&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但這個作法在Vue3 \u002F Nuxt3 不能用了，因為Vite不支援。\u003C\u002Fp>\n\u003Cp>這時要使用另一個Vite的API，\u003Ca href=\"https:\u002F\u002Fvitejs.dev\u002Fguide\u002Ffeatures.html\u002F#glob-import\">\u003Cstrong>\u003Ccode>import.meta.glob\u003C\u002Fcode>\u003C\u002Fstrong>\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>我的理解，\u003Ca href=\"https:\u002F\u002Fvitejs.dev\u002Fguide\u002Ffeatures.html\u002F#glob-import\">\u003Cstrong>\u003Ccode>import.meta.glob\u003C\u002Fcode>\u003C\u002Fstrong>\u003C\u002Fa>有點像是先整理所有引入資料夾中的檔案，建立成一個索引，然後透過key去對應檔名。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;script setup lang=&quot;ts&quot;&gt;\n\nconst glob = import.meta.glob(&#039;@\u002Fassets\u002Fimages\u002F*.jpg&#039;, { eager: true })\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>去檢查 glob 可以看到以下類似資料\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">{\n  &quot;\u002Fassets\u002Fimages\u002Flogo_1`.jpg&quot;: {\n    &quot;default&quot;: &quot;\u002F_nuxt\u002Fassets\u002Fimages\u002Flogo_1.jpg&quot;\n  },\n  &quot;\u002Fassets\u002Fimages\u002Flogo_2.jpg&quot;: {\n    &quot;default&quot;: &quot;\u002F_nuxt\u002Fassets\u002Fimages\u002Flogo_2.jpg&quot;\n  },\n  &quot;\u002Fassets\u002Fimages\u002Flogo_3.jpg&quot;: {\n    &quot;default&quot;: &quot;\u002F_nuxt\u002Fassets\u002Fimages\u002Flogo_3.jpg&quot;\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這時我們用呼叫的圖檔名去尋找真實位置\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img :src=&quot;glob[`\u002Fassets\u002Fimages\u002F${index}.jpg`]&quot; alt=&quot;&quot; \u002F&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>當然也有一些缺點\u003C\u002Fp>\n\u003Cul>\n\u003Cli>圖片名稱會因為Vite快取關係被更改。\u003C\u002Fli>\n\u003Cli>同樣也可能有效能消耗，因為先處理了glob的API。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>就看專案目的取決要使用何種作法囉。\u003C\u002Fp>\n","2024-10-01T00:00:25","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F03%2Ficon-green.svg",{"name":40,"link":41},[98,99],{"name":16,"slug":16},{"name":69,"slug":70},"\u002Fblog\u002Fnuxt3-vue-3-動態取得圖片",{"id":102,"slug":103,"title":104,"description":105,"content":106,"date":107,"image":11,"imageAlt":104,"author":108,"categories":109,"path":111,"minRead":47},3772,"html2canvas-在-safari-中遇到錯誤0ms-starting-document-clone","html2canvas  在 safari 中遇到錯誤”0ms Starting document clone”","html2canvas 在 safari 中遇到錯誤\"0ms Starting document c […]","\u003Ch1>html2canvas  在 safari 中遇到錯誤&quot;0ms Starting document clone&quot;\u003C\u002Fh1>\n\u003Cp>記錄一下。\u003Cbr \u002F>\n如果使用 html2canvas 然後在 IOS 的 safari 上遇到卡住，經過查詢發現錯誤為 &quot;&quot;0ms Starting document clone...&quot; 之類的訊息，應該是因為有圖片元素宣告了 lazy loading，將 lazy 移除或改為 eager 大概就能正常運作了。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;img loading=&quot;eager&quot; src:&quot;xxx&quot;\u002F&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>我遇到問題的環境為 Nuxt3 + html2canvas。\u003C\u002Fp>\n\u003Cp>這個問題應該是因為 html2canvas 在某些狀況下會因為圖片的 lazy 宣告，載入延遲而造成資料卡住，可能新一點的 html2canvas 版本會解決。但我是移除了 lazy 就正常了。\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fniklasvh\u002Fhtml2canvas\u002Fissues\u002F3053\" title=\"參考資料\">參考資料\u003C\u002Fa>\u003C\u002Fp>\n","2024-09-24T21:38:45",{"name":40,"link":41},[110],{"name":16,"slug":16},"\u002Fblog\u002Fhtml2canvas-在-safari-中遇到錯誤0ms-starting-document-clone",[113,119,125,130,135],{"id":114,"databaseId":115,"date":116,"content":117,"authorName":40,"relativeTime":118},"cG9zdDozODE2",3816,"2026-01-12T19:15:36","\u003Cp>現在很多人都會說   我用(AI)做了什麼什麼網站\u003Cbr \u002F>\n等AI內化後\u003Cbr \u002F>\n可能再過幾年就不會特別講了吧.\u003C\u002Fp>\n","2026\u002F1\u002F12",{"id":120,"databaseId":121,"date":122,"content":123,"authorName":40,"relativeTime":124},"cG9zdDozODE1",3815,"2026-01-07T20:16:15","\u003Cp>白噪音播放器 \u003C\u002Fp>\n","2026\u002F1\u002F7",{"id":126,"databaseId":127,"date":128,"content":129,"authorName":40,"relativeTime":124},"cG9zdDozODE0",3814,"2026-01-07T17:44:09","\u003Cp>海商街 重開\u003C\u002Fp>\n",{"id":131,"databaseId":132,"date":133,"content":134,"authorName":40,"relativeTime":124},"cG9zdDozODEz",3813,"2026-01-07T12:00:52","\u003Cp>好冷....呵呵呵\u003C\u002Fp>\n",{"id":136,"databaseId":137,"date":138,"content":139,"authorName":40,"relativeTime":140},"cG9zdDozODEy",3812,"2026-01-06T13:40:18","\u003Cp>好像怪怪的\u003C\u002Fp>\n","2026\u002F1\u002F6"]