<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Kuro Hsu 的筆記</title>
        <link>https://kurohsu.dev</link>
        <description>技術筆記、學習紀錄與生活記錄</description>
        <lastBuildDate>Wed, 20 May 2026 10:38:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh-TW</language>
        <copyright>Copyright © 2025 Kuro Hsu</copyright>
        <atom:link href="https://kurohsu.dev/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Is It Agent Ready? 一個月後的 bot 流量實測]]></title>
            <link>https://kurohsu.dev/notes/agent-ready-one-month-followup.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/agent-ready-one-month-followup.html</guid>
            <pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="is-it-agent-ready-一個月後的-bot-流量實測" tabindex="-1">Is It Agent Ready? 一個月後的 bot 流量實測 <a class="header-anchor" href="#is-it-agent-ready-一個月後的-bot-流量實測" aria-label="Permalink to “Is It Agent Ready? 一個月後的 bot 流量實測”">&#8203;</a></h1>
<p><strong>先說結論：</strong> 就這一個月的觀察下來，把網站弄成「AI 友善」，最老派的方法反而最實在。</p>
<p>一個月前，我把這個部落格的 agent-ready（AI 準備度）分數從 8 分刷到了 58 分。當時憑直覺做了一些設定，現在累積了 4,000 多筆數據，剛好可以回頭驗證。結果發現：當初弄得最花俏、拿最高分的那些設定，其實都只有「打分數的掃描器」在看；而我自認最得意的「專屬乾淨格式」採用率只有 2%。真正把 AI 流量帶進來的，反而是最傳統的 <code>robots.txt</code> 網站宣告。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="is-it-agent-ready-一個月後的-bot-流量實測" tabindex="-1">Is It Agent Ready? 一個月後的 bot 流量實測 <a class="header-anchor" href="#is-it-agent-ready-一個月後的-bot-流量實測" aria-label="Permalink to “Is It Agent Ready? 一個月後的 bot 流量實測”">&#8203;</a></h1>
<p><strong>先說結論：</strong> 就這一個月的觀察下來，把網站弄成「AI 友善」，最老派的方法反而最實在。</p>
<p>一個月前，我把這個部落格的 agent-ready（AI 準備度）分數從 8 分刷到了 58 分。當時憑直覺做了一些設定，現在累積了 4,000 多筆數據，剛好可以回頭驗證。結果發現：當初弄得最花俏、拿最高分的那些設定，其實都只有「打分數的掃描器」在看；而我自認最得意的「專屬乾淨格式」採用率只有 2%。真正把 AI 流量帶進來的，反而是最傳統的 <code>robots.txt</code> 網站宣告。</p>
<hr>
<h2 id="打開儀表板-社群流量還是把-ai-壓著打" tabindex="-1">打開儀表板：社群流量還是把 AI 壓著打 <a class="header-anchor" href="#打開儀表板-社群流量還是把-ai-壓著打" aria-label="Permalink to “打開儀表板：社群流量還是把 AI 壓著打”">&#8203;</a></h2>
<p><img src="/images/notes/agent-ready-followup-traffic.png" alt="dashboard 上的分類聚合：4,266 個事件，social 2041、ai-input 530、search 441、unknown 352、ai-train 232、ai-readiness 204、seo 171、generic-bot 154、human 141"></p>
<p>原本隱約覺得 AI 流量會噴發出一大塊。</p>
<p>結果打開數據一看，有點意外，最大宗的還是大家在 FB 或 LINE 貼網址時，系統自動來抓「預覽圖與摘要」的社群流量，佔了將近一半（48%）。</p>
<p>我把 AI 機器人分成三類：「為了回答網友問題來查資料的」（像 ChatGPT）、「到處抓文章回去當訓練材料的」（像 GPTBot），以及「單純來打分數的掃描器」。這三類加起來只佔了 22%，比想像中少，而且幾乎都是 ChatGPT 跟 Perplexity 帶來的。</p>
<p><strong>第一個校正：</strong> AI 流量確實起來了，但傳統的社群分享本來就是這個 blog 的命脈，AI 流量還沒大到能把它壓過去。</p>
<h2 id="well-known-的-666-次請求看起來最熱鬧-拆開全是同一個東西" tabindex="-1">well-known 的 666 次請求看起來最熱鬧，拆開全是同一個東西 <a class="header-anchor" href="#well-known-的-666-次請求看起來最熱鬧-拆開全是同一個東西" aria-label="Permalink to “well-known 的 666 次請求看起來最熱鬧，拆開全是同一個東西”">&#8203;</a></h2>
<p><img src="/images/notes/agent-ready-followup-events.png" alt="dashboard 上的事件類型分布：bot_article 3,557、well_known 666、markdown_negotiation 43，加總 4,266"></p>
<p>看數據時發現，<code>/.well-known</code>（一個專門放 AI 說明書的隱藏路徑）被打了 666 次，當下想說「哇，這麼受歡迎？」</p>
<p>結果攤開一看：
全都是「評分掃描器」跟「Chrome 瀏覽器的預載功能」在自嗨。真正的 AI（像 ChatGPT）根本沒來戳過這裡。</p>
<p>更好笑的是，後台留了 600 多次「找不到網頁 (404)」的錯誤紀錄。原來是掃描器瘋狂在問我當初故意不做的那些「拿高分專用端點」。這證明了原文裡的決定是對的：千萬不要光看「請求次數」就以為有市場需求，跑去硬寫那些功能，真的會被掃描器騙去白忙一場。</p>
<h2 id="那個我最得意的改造-採用率-2" tabindex="-1">那個我最得意的改造，採用率 2% <a class="header-anchor" href="#那個我最得意的改造-採用率-2" aria-label="Permalink to “那個我最得意的改造，採用率 2%”">&#8203;</a></h2>
<p>這段真的是自己打臉。</p>
<p>上次改造中，我最得意的就是「Markdown 內容協商」，也就是說，只要 AI 告訴我「我想看乾淨的文字 (Markdown)」，我就直接給它，幫它省去解讀複雜網頁標籤 (HTML) 的麻煩。</p>
<p>結果這 28 天下來：
AI 總共來了 500 多次，只有 10 次主動要求這個乾淨格式，採用率慘不忍睹的只有 2%。而唯一 100% 捧場用這功能的，笑死，全都是我自己拿工具在手動測試的紀錄。</p>
<p>技術上這機制設計得很漂亮，但現實是：現在的主流 LLM 早就習慣硬啃 HTML 了，它們根本不會主動要求乾淨格式。當初覺得「架構乾淨＝會被大量採用」，實在跳太快了。當成一個長線投資可以，但當下它就是只有 2% 的採用率。</p>
<h2 id="_5-8-那天到底發生什麼事" tabindex="-1">5/8 那天到底發生什麼事 <a class="header-anchor" href="#_5-8-那天到底發生什麼事" aria-label="Permalink to “5/8 那天到底發生什麼事”">&#8203;</a></h2>
<p>平常每天流量大概 100 多次，5/8 那天突然衝到快 300 次。那天我也沒發新文章，第一反應是「哪個爬蟲壞掉了？」。</p>
<p>拆開 Log 一看，原來是 Perplexity、GPTBot 這些大廠的爬蟲，終於把這個 blog 排進了「定期巡邏名單」，連兩三年前的老文章都被它們翻出來重新看過一輪。</p>
<p><strong>順便發現的雷達：</strong> 做完這些設定後，大概要等個「一到兩週」，大廠的生態系才會真正把你掃進去。太早看數據沒什麼意義，因為大環境還沒認識你。</p>
<h2 id="原本寫最短的那條-反而帶量最大" tabindex="-1">原本寫最短的那條，反而帶量最大 <a class="header-anchor" href="#原本寫最短的那條-反而帶量最大" aria-label="Permalink to “原本寫最短的那條，反而帶量最大”">&#8203;</a></h2>
<p>上次文章裡我只簡單帶過 <code>robots.txt</code>（傳統的爬蟲規則）的設定，覺得最沒梗，結果把資料攤開看，它才是真正的大魔王。</p>
<p><code>bot_article</code> 3,557 個事件佔全部 83%。真正帶 bot 流量的就是這層：bot 透過正常爬蟲管道進來抓 HTML 文章頁。</p>
<p><img src="/images/notes/agent-ready-followup-pie.png" alt="Traffic composition pie chart — social 47.9%、ai-input 12.4%、search 10.3%、unknown 8.3%、ai-train 5.4%"></p>
<p>當初設定 <code>robots.txt</code> 時，我用 Content Signals 標了偏好：可以即時回答網友（<code>ai-input=yes</code>），但別當訓練資料（<code>ai-train=no</code>）。從數據看，AI 真的有在尊重這個宣告，被引用去回答問題的次數是當訓練資料的 2.2 倍（單看上一篇甚至到 5:1）。</p>
<p>這點直接改變了我的寫作策略：以後寫技術文，與其寫落落長的長文去拚 Google 搜尋，不如把每個段落的「結論」寫清楚，讓 AI 好摘錄。一篇文章被 ChatGPT 抓 30 次去回答網友，那種感覺跟單純被 Google 索引是完全不同的，它是真正意義上的「被即時引用」。</p>
<h2 id="is-it-agent-ready-真的-ready-嗎" tabindex="-1">Is It Agent Ready? 真的 Ready 嗎 <a class="header-anchor" href="#is-it-agent-ready-真的-ready-嗎" aria-label="Permalink to “Is It Agent Ready? 真的 Ready 嗎”">&#8203;</a></h2>
<p>測驗工具給了我 58 分，看起來是 Ready 了。但看完這 28 天的數據，我會這樣解讀：</p>
<p>真正帶來 83% 流量的，是最傳統的 <code>robots.txt</code> 宣告，因為所有 AI 本來就懂這套老規矩。
而讓我從 8 分衝到 58 分的那些「進階 AI 改造」，真實 AI 的使用率幾乎是零。</p>
<p>這個分數，量測的其實是「對掃描器的友善度」；至於「AI 真的會怎麼用你的站」，它根本量不到。基礎建設我們已經鋪好了，但那些進階的玩法，目前只能等整個 AI 生態系慢慢跟上。也許半年後再回來看，那 2% 的採用率才會開始往上爬吧。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vitepress</category>
            <category>ai</category>
            <category>agents</category>
            <category>analytics</category>
            <category>seo</category>
        </item>
        <item>
            <title><![CDATA[AI Agent 時代的 Vue 前端開發：從 UX 走向 AX]]></title>
            <link>https://kurohsu.dev/learn/vue-frontend-ux-to-ax.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/learn/vue-frontend-ux-to-ax.html</guid>
            <pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="ai-agent-時代的-vue-前端開發-從-ux-走向-ax" tabindex="-1">AI Agent 時代的 Vue 前端開發：從 UX 走向 AX <a class="header-anchor" href="#ai-agent-時代的-vue-前端開發-從-ux-走向-ax" aria-label="Permalink to “AI Agent 時代的 Vue 前端開發：從 UX 走向 AX”">&#8203;</a></h1>
<p>AI Agent 時代的前端開發，正在從 UX（User Experience）延伸到 AX（Agent Experience），也就是把網站同時設計給人類跟 AI agent 使用。對 Vue 開發者來說，這代表 component、store、route 跟 tool 之間的關係要重新整理一輪。WebMCP 這類規格只是入口；核心變化是前端工程師的工作範圍會擴大，以前歸在後端 API doc 範圍的 tool 命名、description 文案、權限邊界，正在往瀏覽器這一端推。</p>
<p>幾天前我寫了一篇〈<a href="/learn/learning-webmcp.html">讓網站直接跟 AI Agent 對話：初試 WebMCP</a>〉，聚焦在規格本身、<code>navigator.modelContext</code> 怎麼用、demo 怎麼跑，想了解 WebMCP 細節的話建議先看那篇。寫完之後我意識到還有一塊沒寫：身為一個寫了多年 Vue 的前端開發者，我自己面對這波浪潮時在想什麼？Vue 的哪些結構特性剛好對得上 AX、哪些反而會變成負擔？這篇就是回答這個問題的個人筆記。</p>
<p><img src="/images/learn/vue-frontend-ax-hero.jpg" alt="「從 UX 到 AX：AI Agent 時代的響應式設計」橫幅示意圖。左側「UX：人類體驗 · 視覺互動」展示工程師在筆電與手機前操作 ACME 數位體驗網站；中央以流動箭頭連接「人類視覺介面 → 結構化 tool call」，標註「同一個服務 · 兩種體驗，UX 滿足人類、AX 服務 AI Agent，相輔相成擴大價值與觸及」；右側「AX：代理體驗 · 結構化互動」展示機器人面對 JSON 結構的 tool call 視窗，範例為查詢上週銷售資料並產生摘要報告，包含意圖理解、工具選擇 get_sales_data 與參數、結構化回應、執行後續行動。底部時間軸：桌面網站時代（Web 1.0 / 2.0）→ 行動響應式設計（Mobile First）→ AI Agent 時代（AX: Agent Experience）。"></p>
]]></description>
            <content:encoded><![CDATA[<h1 id="ai-agent-時代的-vue-前端開發-從-ux-走向-ax" tabindex="-1">AI Agent 時代的 Vue 前端開發：從 UX 走向 AX <a class="header-anchor" href="#ai-agent-時代的-vue-前端開發-從-ux-走向-ax" aria-label="Permalink to “AI Agent 時代的 Vue 前端開發：從 UX 走向 AX”">&#8203;</a></h1>
<p>AI Agent 時代的前端開發，正在從 UX（User Experience）延伸到 AX（Agent Experience），也就是把網站同時設計給人類跟 AI agent 使用。對 Vue 開發者來說，這代表 component、store、route 跟 tool 之間的關係要重新整理一輪。WebMCP 這類規格只是入口；核心變化是前端工程師的工作範圍會擴大，以前歸在後端 API doc 範圍的 tool 命名、description 文案、權限邊界，正在往瀏覽器這一端推。</p>
<p>幾天前我寫了一篇〈<a href="/learn/learning-webmcp.html">讓網站直接跟 AI Agent 對話：初試 WebMCP</a>〉，聚焦在規格本身、<code>navigator.modelContext</code> 怎麼用、demo 怎麼跑，想了解 WebMCP 細節的話建議先看那篇。寫完之後我意識到還有一塊沒寫：身為一個寫了多年 Vue 的前端開發者，我自己面對這波浪潮時在想什麼？Vue 的哪些結構特性剛好對得上 AX、哪些反而會變成負擔？這篇就是回答這個問題的個人筆記。</p>
<p><img src="/images/learn/vue-frontend-ax-hero.jpg" alt="「從 UX 到 AX：AI Agent 時代的響應式設計」橫幅示意圖。左側「UX：人類體驗 · 視覺互動」展示工程師在筆電與手機前操作 ACME 數位體驗網站；中央以流動箭頭連接「人類視覺介面 → 結構化 tool call」，標註「同一個服務 · 兩種體驗，UX 滿足人類、AX 服務 AI Agent，相輔相成擴大價值與觸及」；右側「AX：代理體驗 · 結構化互動」展示機器人面對 JSON 結構的 tool call 視窗，範例為查詢上週銷售資料並產生摘要報告，包含意圖理解、工具選擇 get_sales_data 與參數、結構化回應、執行後續行動。底部時間軸：桌面網站時代（Web 1.0 / 2.0）→ 行動響應式設計（Mobile First）→ AI Agent 時代（AX: Agent Experience）。"></p>
<hr>
<h2 id="從-ux-到-ax-ai-agent-時代的響應式設計" tabindex="-1">從 UX 到 AX：AI Agent 時代的響應式設計 <a class="header-anchor" href="#從-ux-到-ax-ai-agent-時代的響應式設計" aria-label="Permalink to “從 UX 到 AX：AI Agent 時代的響應式設計”">&#8203;</a></h2>
<p>打開這個話題，日本工程師 takoratta 在<a href="https://takoratta.hatenablog.com/entry/2026/04/28/112859" target="_blank" rel="noreferrer">一篇文章</a>裡把 AX 類比成「AI 時代的響應式設計」，這個比喻蠻有意思的，我讀完之後覺得確實是這樣。</p>
<p>回想一下 web 過去三十年的兩次大轉折。90 年代末瀏覽器戰爭打完，W3C 把 HTML / CSS / JS 標準收束起來，讓網站不用為每個瀏覽器寫一份程式碼。
2010 年前後智慧型手機普及，新的用戶端類型（行動裝置）冒出來，網站得學會同時服務不同螢幕尺寸的訪客，Responsive Web Design（RWD）變成共識。
2026 年的此刻，新的用戶端又出現了：AI agent。它沒有眼睛、不看畫面、不點按鈕，讀的是結構化的 tool schema 跟自然語言描述。
網站得再學一次「同時服務多種前提的訪客」，這次的差異在於「人類視覺 vs 結構化 tool call」。</p>
<p><img src="/images/learn/vue-frontend-ax-timeline.jpg" alt="深色科技風橫向時間軸示意圖：左側是 90 年代 CRT 桌機與地球符號，代表早期 web；中央是平板、手機、筆電圍繞一塊發光的平台，代表 RWD 與行動裝置時代；右側是機器人指著流程圖、對話泡泡與資料庫節點，代表 AI agent 操作網站的新階段，三個階段以光帶串連，呼應文章從 UX 走向 AX 的歷史敘事。"></p>
<p>不過這個類比也有邊界。RWD 處理的是呈現層配接，「換個 viewport 看到合適版本」就完事；AX 處理的是能力暴露跟操作邊界，「agent 能呼叫什麼、傳什麼、後果是什麼」。所以 AX 的安全性、權限模型、稽核這些東西會比 RWD 更核心，光把 RWD 經驗搬過來不夠用。</p>
<p>換個角度看，網站的訪客正在分化成三種：人類、傳統 crawler（搜尋引擎、AI 訓練爬蟲）、即時操作的 agent。前兩種都還是「讀」資訊，差別只在誰來消化；第三種是「操作」資訊，讀完之後會直接呼叫 tool 改變狀態。AX 真正想處理的是這第三種訪客，他們對網站的期待已經超過「給我可讀的 HTML」這層，需要的是一份可呼叫的能力清單。</p>
<p>這就是 AX 想處理的問題。</p>
<h3 id="ax-是什麼" tabindex="-1">AX 是什麼 <a class="header-anchor" href="#ax-是什麼" aria-label="Permalink to “AX 是什麼”">&#8203;</a></h3>
<p>AX 是 Agent Experience 的縮寫，由 Netlify 共同創辦人 Matt Biilmann 提出，<a href="https://agentexperience.ax/" target="_blank" rel="noreferrer">agentexperience.ax</a> 上有官方定義：「AI agent 在跟產品、平台或系統互動時得到的整體體驗」。它跟 UX（User Experience）、DX（Developer Experience）是同一個概念家族，差別只在受眾從人類使用者、開發者擴大到了 agent。</p>
<p>套到網站場景，AX 講的是當 agent（語言模型、瀏覽器內建的 AI、自動化腳本）成為訪客時，我們為這類非人類用戶端設計的操作體驗。過去網站的預設訪客只有一種，有眼睛、會操作滑鼠跟觸控的人類，所有資訊都塞在渲染完的視覺層；agent 想介入只能靠截圖猜按鈕位置或爬 DOM 反推語意，品質一直很差。WebMCP 跟 NLWeb 這類規格冒出來，等於是讓網站第一次能對 agent 說「這頁有哪些操作、輸入是什麼、後果是什麼」。AX 在描述的就是這種對話品質。</p>
<p>要先 caveat 一下，這篇主要從 WebMCP 角度切入 AX，但 AX 本身是更廣的概念，跟 NLWeb、Browser Agent、未來的其他 agent-facing API 都有交集。WebMCP 只是目前最具體可動手的一條路徑。</p>
<p>跟 UX 對照看比較容易抓到邊界。UX 想的是視覺層級、互動回饋、頁面節奏這類人眼在意的事；AX 想的是 tool 命名要不要白話、description 文案夠不夠精準讓 LLM 一次選對、錯誤訊息有沒有告訴 agent「為什麼錯、要怎麼修」、哪些動作要強制人類二次確認。失敗模式也不一樣，UX 失敗是使用者點不到、找不到；AX 失敗是 agent 選錯工具、塞錯參數、無腦重試把 server 打掛。</p>
<table tabindex="0">
<thead>
<tr>
<th>維度</th>
<th>UX</th>
<th>AX</th>
</tr>
</thead>
<tbody>
<tr>
<td>用戶端</td>
<td>人類</td>
<td>agent / LLM</td>
</tr>
<tr>
<td>主要輸入</td>
<td>視覺、滑鼠、觸控</td>
<td>結構化 tool schema、自然語言描述</td>
</tr>
<tr>
<td>失敗模式</td>
<td>點不到、看不懂</td>
<td>選錯工具、參數不符、誤觸高風險動作</td>
</tr>
<tr>
<td>設計重點</td>
<td>視覺層級、互動回饋、文案語氣</td>
<td>tool 命名、description 文案、權限邊界、agent 友善的錯誤訊息</td>
</tr>
<tr>
<td>維運債</td>
<td>UI 跟設計稿 drift</td>
<td>UI 跟 tool definition drift</td>
</tr>
</tbody>
</table>
<h3 id="ax-跟-seo-a11y-semantic-html-的關係" tabindex="-1">AX 跟 SEO / a11y / semantic HTML 的關係 <a class="header-anchor" href="#ax-跟-seo-a11y-semantic-html-的關係" aria-label="Permalink to “AX 跟 SEO / a11y / semantic HTML 的關係”">&#8203;</a></h3>
<p>AX 容易被混進現有的 SEO、accessibility、semantic HTML 這幾條既有討論裡。我自己一開始也想過「這不就是 a11y 換個名詞嗎」，後來想清楚三者其實在處理不同層面的問題。SEO 處理「內容能不能被搜尋引擎理解、進 index、出現在搜尋結果」；accessibility 處理「人類使用者（特別是有輔助科技需求的）能不能順利操作網站」；AX 處理「agent 能不能搞清楚這個網站允許我做哪些事、要丟什麼參數、做了會發生什麼後果」。</p>
<p>三者會共享一些基礎，例如語意化 HTML、清楚的狀態、穩定的資訊結構。但 AX 多出一層 capability design：網站除了被讀，還可以被呼叫。這層是 SEO 跟 a11y 都沒在處理的。</p>
<hr>
<h2 id="ax-把哪些工作推到前端" tabindex="-1">AX 把哪些工作推到前端 <a class="header-anchor" href="#ax-把哪些工作推到前端" aria-label="Permalink to “AX 把哪些工作推到前端”">&#8203;</a></h2>
<p>AX 對前端工程師最直接的影響，是工作邊界正在重畫。</p>
<p>過去前端跟後端有一條相對清楚的分工線：後端負責 API 設計、命名、文案、權限模型；前端負責把資料拿來渲染、處理互動。tool 命名好不好、description 文案精不精確、權限邊界畫在哪，這些是 API doc 的事，前端讀文件來用就好。</p>
<p>WebMCP 把這條線往前端拉了。至少從目前 draft API 的形態來看，tool 註冊發生在瀏覽器，跟著 component 生命週期走，跟 UI 文案共用同一份 i18n，輸入 schema 通常借用前端已有的 form validation library（Zod、VeeValidate）。換句話說，以前你寫一份 API spec 給後端，他幫你做一支 endpoint 出來；現在你直接在 Vue component 裡 <code>registerTool</code>，description 文案就是你寫，輸入 schema 就是你的 form schema，errors 訊息就是你 UI 顯示的那份。</p>
<p>工作量轉移之外，連帶把幾個過去看似後端責任的東西，實質變成前端設計問題：</p>
<p><strong>Tool 命名跟 description 文案</strong>
LLM 選工具靠的就是這兩個欄位，寫得好可以一次選對，寫不好就會誤呼叫或無腦重試。這是寫作問題，需要思考的是：讀者（模型）在什麼情境會看到這段文字、它需要什麼資訊才能做正確判斷。前端工程師寫 UX 文案累積的肌肉記憶其實很接近，只是受眾換了。</p>
<p><strong>權限邊界</strong>
哪些 tool 可以給 agent 自由呼叫、哪些必須走 <code>requestUserInteraction</code>、哪些角色才看得到、跨頁的時候要不要回收，這些以前是後端 authorization 在管的事，現在會散落在 component 跟 store 裡。Vue 開發者過去靠 router guard 跟 provide/inject 處理 UI 層權限，同一套機制現在要延伸到 tool 層。</p>
<p><strong>錯誤訊息的 agent 版本</strong>
給人類看的錯誤訊息可以是「請稍後再試」，給 agent 看必須是結構化、可動作的：為什麼錯、缺什麼參數、要怎麼修、能不能重試。前端要學會同一個錯誤寫兩份文案，一份給人、一份給 agent。</p>
<h3 id="ui-跟-tool-definition-的-drift-問題" tabindex="-1">UI 跟 tool definition 的 drift 問題 <a class="header-anchor" href="#ui-跟-tool-definition-的-drift-問題" aria-label="Permalink to “UI 跟 tool definition 的 drift 問題”">&#8203;</a></h3>
<p>這幾個工作推到前端之後，會浮現一個維運核心問題：<strong>UI 跟 tool definition 各改各的</strong>。</p>
<p>我自己第一次體會到嚴重性，是試著把 demo 裡的搜尋欄位改了一個欄位名，UI 沒事（因為是表單渲染），但對應的 <code>search_products</code> tool 的 inputSchema 我忘了改，agent 還在用舊欄位名呼叫。Browser 端不會報錯，只回 200 加空陣列，模型以為「沒搜到」就再試一次，變成失敗迴圈。</p>
<p>這種 drift 在純後端 API 時代不太發生，因為 API 跟 UI 之間有 contract，改 API 一定要改 client，IDE 會跳紅字。但 WebMCP 的 tool 定義跟 UI 都在前端，沒有編譯時期的強制連動，改 UI 忘記改 tool 是最容易發生的事。</p>
<p>這是 AX 帶給前端工程師最具體的維運債，也是接下來看 Vue 結構優勢的時候，最值得拿來檢驗各種設計選擇的標準：<strong>這個寫法會讓 drift 變嚴重，還是減輕？</strong></p>
<hr>
<h2 id="為什麼我認為-vue-js-適合-ax" tabindex="-1">為什麼我認為 Vue.js 適合 AX？ <a class="header-anchor" href="#為什麼我認為-vue-js-適合-ax" aria-label="Permalink to “為什麼我認為 Vue.js 適合 AX？”">&#8203;</a></h2>
<p>走完 AX 的概念框架，接下來想拉回到具體技術棧上。我自己寫了多年 Vue，在實際試做 WebMCP demo 的時候，有幾個瞬間意識到一件事：<strong>AX 場景需要的東西，Vue 的 mental model 早就準備好了</strong>。以下整理五個契合點。</p>
<h3 id="reactivity-跟-tool-狀態天生匹配" tabindex="-1">Reactivity 跟 tool 狀態天生匹配 <a class="header-anchor" href="#reactivity-跟-tool-狀態天生匹配" aria-label="Permalink to “Reactivity 跟 tool 狀態天生匹配”">&#8203;</a></h3>
<p>WebMCP 最核心的循環是：<strong>agent 觀察狀態 → 呼叫 tool → 觀察狀態變化</strong>。Vue 的 reactivity 系統把這件事做到極致，tool 內部改一個 ref 或 store，UI 自動 re-render，agent 下一次讀畫面就看到新狀態。</p>
<p>我自己寫 React 寫到類似場景時，常會在 hook dependency、closure freshness、memoization 邊界這些地方多花力氣；Vue 的 ref / computed / store 模型比較貼近「狀態變了，UI 跟 agent 的觀察結果會一起更新」這種直覺，這層在 Vue 幾乎零成本。對 AX 場景特別重要，因為 agent 每次呼叫 tool 預期的是「執行完狀態就該變」，如果中間有同步問題，agent 會以為操作失敗去重試。</p>
<p><img src="/images/learn/vue-frontend-ax-reactivity-loop.jpg" alt="Vue Reactivity × WebMCP Tool State 循環示意圖。標題「Reactivity × Tool State（Vue + WebMCP）」。左側是 Vue 應用的儀表板，顯示任務完成數 12、狀態完成、進度 78% 與任務清單；中央是四步循環：1. 觀察狀態 → 2. 呼叫 Tool（WebMCP tool.call processData，執行中→執行成功）→ 3. 狀態更新 → 4. 自動重繪；右側是 AI agent 觀察兩個快照，觀察（目前）顯示任務完成數 12、狀態處理中，觀察（更新後）顯示任務完成數 15、狀態完成；右下角的「stale sync, retry」警告被劃掉，代表 Vue reactivity 自動處理掉 React 場景下需要手動解決的 stale closure 與同步重試問題。"></p>
<h3 id="pinia-action-是-tool-wrapper-的好入口" tabindex="-1">Pinia action 是 tool wrapper 的好入口 <a class="header-anchor" href="#pinia-action-是-tool-wrapper-的好入口" aria-label="Permalink to “Pinia action 是 tool wrapper 的好入口”">&#8203;</a></h3>
<p>Vue.js 開發生態圈的 Pinia action 在形狀上很接近一個 tool：具名、集中副作用、可回傳結果。
把現有的 store action 用 <code>registerTool</code> 包一層，agent 立刻就能呼叫，而且因為 action 本來就是 component 在用的，等於人類使用者跟 agent 共用同一份業務邏輯，不會分裂。</p>
<p>一個 Pinia action 要真的變成 production-ready 的 tool，我自己評估至少要補五件事：</p>
<ul>
<li><strong>runtime input schema</strong>（Zod 或 valibot，光靠 TypeScript 型別不夠，因為 agent 傳的是 JSON）</li>
<li><strong>agent-readable description</strong>（寫給 LLM 看的自然語言文案，跟給 IDE intellisense 看的 JSDoc 是兩回事）</li>
<li><strong>權限檢查</strong>（這個 action 該不該開放給 agent、目前角色能不能用）</li>
<li><strong>agent-friendly 錯誤語意</strong>（回傳結構化原因，不只是 throw）</li>
<li><strong>audit log</strong>（這次呼叫是 agent 還是人類觸發、傳了什麼參數）</li>
</ul>
<p><img src="/images/learn/vue-frontend-ax-pinia-tool-wrapper.jpg" alt="Pinia action 包裝成 WebMCP tool 的流程示意圖。左側 Vue 應用畫面與綠色「Pinia Action」區塊（V 標誌）透過 Shared Logic 流向中央藍色「Tool Wrapper」立方體；Tool Wrapper 周圍環繞五個必補的關鍵元件：Schema（輸入結構）、Auth（權限檢查）、Desc（自然語言描述）、Error（錯誤語意）、Audit（稽核紀錄）；右側 WebMCP 面板顯示 registerTool() 與 invoke() 的 JSON 結構，最右邊是呼叫 tool 的 AI agent。對應文章中「Pinia action 不會自動 1:1 對應到 production-ready tool，需要補這五件事」的論點。"></p>
<p>寫一個 <code>defineMcpTool()</code> helper 從 store 自動註冊只是起點，真正的工作量在補上面這五件事。
但起點接近終點這件事仍是 Vue 的優勢，React 那邊把分散在 reducer / hook / context 的副作用收斂成 tool 起手式更遠。</p>
<h3 id="composition-api-生命週期-tool-scope" tabindex="-1">Composition API 生命週期 = tool scope <a class="header-anchor" href="#composition-api-生命週期-tool-scope" aria-label="Permalink to “Composition API 生命週期 = tool scope”">&#8203;</a></h3>
<p><code>onMounted</code> 註冊、<code>onBeforeUnmount</code> 取消註冊，這個 pattern 把 tool 的可用範圍跟 component 生命週期綁在一起，做出來的效果是：<strong>agent 看到的工具集會跟著使用者導航自然變化</strong>。</p>
<p>舉個例子，結帳頁才出現的 <code>applyCoupon</code> tool，在使用者離開結帳頁的瞬間就消失。對 LLM 而言這是大幅降噪，看到 5 個情境相關 tool 比看到 50 個全站 tool 選擇品質好太多。Composition API 的 setup 函式天然就是執行這種註冊邏輯的地方，寫起來像呼吸一樣自然。</p>
<h3 id="sfc-讓-tool-定義跟-ui-住在一起-抑制-drift" tabindex="-1">SFC 讓 tool 定義跟 UI 住在一起，抑制 drift <a class="header-anchor" href="#sfc-讓-tool-定義跟-ui-住在一起-抑制-drift" aria-label="Permalink to “SFC 讓 tool 定義跟 UI 住在一起，抑制 drift”">&#8203;</a></h3>
<p>回到上一節說的 drift 問題。SFC 是 Vue 對抗這個問題的天然優勢：tool 定義可以寫在控制它的 component 旁邊，改 UI 的時候很難不看到 tool 定義也得改。</p>
<p>對比一下集中註冊 pattern：有的設計是把所有 tool 集中在一個 <code>tools.ts</code> 或 <code>mcp-server.ts</code> 裡定義，改 UI 的人根本不會開啟那個檔案，drift 機率最大化。SFC 把兩者放在同一個檔案的兩個 <code>&lt;script&gt;</code> 段，physical proximity 帶來的 cognitive coupling 是零成本的維運債抵抗力。</p>
<p>這當然不是銀彈，跨頁流程的 tool 還是得放 store 或全域 composable，不能塞 component。但對「這頁能做的事」這類常見場景，SFC 結構是最佳解。</p>
<h3 id="vue-router-的-meta-pinia-補完權限與-context" tabindex="-1">vue-router 的 meta + Pinia 補完權限與 context <a class="header-anchor" href="#vue-router-的-meta-pinia-補完權限與-context" aria-label="Permalink to “vue-router 的 meta + Pinia 補完權限與 context”">&#8203;</a></h3>
<p>最後一塊是路由跟 context。WebMCP tool 常常需要知道目前的登入狀態、feature flag、使用者角色，如果每個 tool 自己拉這些資訊會很重。</p>
<p>Vue 的標準作法裡，這類共享 context 通常已經住在 Pinia store（<code>useAuthStore</code>、<code>useFeatureFlagStore</code>），<code>useMcpTool</code> 直接 <code>import</code> 過來用即可。vue-router 則補上「路由級」這層：用 <code>route.meta.requiresAdmin</code> 標註權限門檻，讓 tool 註冊邏輯讀 meta 決定要不要曝露 tool 給 agent。</p>
<p>具體配合方式長這樣。先在 vue-router 的 routes 定義裡標權限：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// router/routes.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { RouteRecordRaw } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue-router'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> routes</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> RouteRecordRaw</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    path: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/admin/users'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'@/views/admin/UserList.vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    meta: { requiresAdmin: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 其他路由 ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><p>接著在 admin 頁面 component 的 <code>&lt;script setup lang=&quot;ts&quot;&gt;</code> 內，讀 <code>route.meta</code> 跟 Pinia auth store 一起判斷，條件不符就乾脆不呼叫 <code>useMcpTool</code>，agent 自然看不到這個 tool：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// components/admin/UserActions.vue 的 &#x3C;script setup lang="ts"> 內容</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useRoute } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue-router'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useAuthStore } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@/stores/auth'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useMcpTool } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@/composables/useMcpTool'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> route</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useRoute</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> auth</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useAuthStore</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 雙重檢查：路由標 requiresAdmin + 當前角色是 admin</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (route.meta.requiresAdmin </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> auth.role </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'admin'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  useMcpTool</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'delete_user'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    description: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'刪除指定使用者帳號（永久動作，僅 admin 可呼叫）'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    inputSchema: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'object'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      properties: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        userId: { type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'string'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, description: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'要刪除的使用者 ID'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      required: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'userId'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> execute</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">input</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">userId</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> res</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`/api/admin/users/${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">input</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">userId</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        method: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'DELETE'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      })</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        content: [{ type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'text'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, text: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> res.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">json</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()) }]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br></div></div><p>整套機制都在 Vue 既有 mental model 之內，不用引入額外的依賴注入機制。Server 端當然還要再驗一次權限，client-side 條件式註冊只是降低 tool 暴露面，不能當成最終守線。</p>
<hr>
<h2 id="vue-開發者的最小切入點" tabindex="-1">Vue 開發者的最小切入點 <a class="header-anchor" href="#vue-開發者的最小切入點" aria-label="Permalink to “Vue 開發者的最小切入點”">&#8203;</a></h2>
<p>理論講完，實作上我自己會建議的最小切入路徑長這樣。</p>
<h3 id="第一步-從-read-only-tool-開始" tabindex="-1">第一步：從 read-only tool 開始 <a class="header-anchor" href="#第一步-從-read-only-tool-開始" aria-label="Permalink to “第一步：從 read-only tool 開始”">&#8203;</a></h3>
<p>不要一開始就把寫入類 tool 開出來。先選 3 到 5 個 read-only 的查詢類 action（搜尋、篩選、看詳情、看清單），包成 tool 觀察 agent 行為。這個階段的目標只有兩個：</p>
<ul>
<li><strong>觀察 LLM 怎麼選擇你的 tool</strong>：description 寫得夠不夠精準？常常選錯的話文案要怎麼改？</li>
<li><strong>觀察 agent 的呼叫頻率跟錯誤模式</strong>：會不會無腦重試？errors 訊息夠不夠 agent 友善？</li>
</ul>
<p>跑兩三天之後再決定要不要開寫入類 tool。這比一次全開安全很多，也讓你有時間建立 audit log 跟監控。</p>
<h3 id="第二步-pinia-action-包成-tool-的範本" tabindex="-1">第二步：Pinia action 包成 tool 的範本 <a class="header-anchor" href="#第二步-pinia-action-包成-tool-的範本" aria-label="Permalink to “第二步：Pinia action 包成 tool 的範本”">&#8203;</a></h3>
<p>我自己用的範本大概長這樣（假設用 Pinia + Zod，比舊文 demo 多了 store 整合跟 schema 重用）：</p>
<p>先在 <code>stores/products.ts</code> 集中業務邏輯：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// stores/products.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { defineStore } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'pinia'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { ref } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">interface</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Product</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">id</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">name</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">price</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> useProductStore</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineStore</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'products'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> items</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Product</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[]>([])</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> searchProducts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">input</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">query</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">maxPrice</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> params</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> URLSearchParams</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    params.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">set</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'query'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, input.query)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (input.maxPrice </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) params.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">set</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'maxPrice'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(input.maxPrice))</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> res</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/api/search?'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> params.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">())</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    items.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> res.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">json</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> items.value</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { items, searchProducts }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br></div></div><p>接著抽一個極薄的 composable 處理 WebMCP 註冊與生命週期：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// composables/useMcpTool.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { onMounted } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">type</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> McpToolDefinition</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  name</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  description</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  inputSchema</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> unknown</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  annotations</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Record</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">unknown</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  execute</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">input</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;{</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    content</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;{ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">type</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'text'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">text</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useMcpTool</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">definition</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> McpToolDefinition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  onMounted</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">typeof</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> navigator </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'undefined'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'modelContext'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> in</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> navigator)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    ;(navigator </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> any</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).modelContext.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">registerTool</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(definition)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 規格目前還沒明確的 unregister API，現階段先把註冊綁在 component lifecycle 上，真正的 dispose 等規格補完再接</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br></div></div><p>最後在 component 的 <code>&lt;script setup lang=&quot;ts&quot;&gt;</code> 內把 store action 包成 tool，schema 重用同一份 Zod 定義：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// components/ProductSearch.vue 的 &#x3C;script setup lang="ts"> 內容</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { z } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'zod'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { zodToJsonSchema } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'zod-to-json-schema'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useMcpTool } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@/composables/useMcpTool'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useProductStore } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@/stores/products'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> store</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useProductStore</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> searchInputSchema</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> z.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">object</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  query: z.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">describe</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'搜尋關鍵字'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  maxPrice: z.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">number</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">optional</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">describe</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'價格上限，單位 TWD'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">useMcpTool</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'search_products'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  description: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'搜尋咖啡豆商品。支援關鍵字、價格上限過濾。回傳符合條件的商品列表。'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  inputSchema: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">zodToJsonSchema</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(searchInputSchema),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  annotations: { readOnlyHint: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> execute</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">input</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> parsed</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> searchInputSchema.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(input)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> store.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">searchProducts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(parsed)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { content: [{ type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'text'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, text: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(result) }] }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br></div></div><p>幾個重點。<code>searchInputSchema</code> 這個 Zod schema 同時餵給 tool 的 inputSchema 跟 <code>execute</code> 的 runtime parse，確保 agent 傳什麼進來都先過一次驗證。description 寫得比一般 JSDoc 仔細，因為 LLM 沒有其他資訊可參考。<code>useMcpTool</code> 是極薄的 wrapper，只處理特性偵測跟生命週期，不做任何業務假設。</p>
<h3 id="第三步-動態-scope-tool-跟著畫面或路由走" tabindex="-1">第三步：動態 scope tool，跟著畫面或路由走 <a class="header-anchor" href="#第三步-動態-scope-tool-跟著畫面或路由走" aria-label="Permalink to “第三步：動態 scope tool，跟著畫面或路由走”">&#8203;</a></h3>
<p>不要把所有 tool 在 app 啟動時一次註冊完。用 component 生命週期或路由切換做動態 scope：</p>
<ul>
<li>結帳頁才註冊 <code>apply_coupon</code>、<code>place_order</code></li>
<li>商品列表頁才註冊 <code>add_to_cart</code></li>
<li>後台才註冊 <code>delete_user</code>、<code>reset_password</code></li>
</ul>
<p>這樣 agent 在任何時刻看到的 tool 集合都跟使用者當下情境對齊，選擇品質好很多。</p>
<h3 id="第四步-別太早抽-usemcp-框架層" tabindex="-1">第四步：別太早抽 <code>useMcp()</code> 框架層 <a class="header-anchor" href="#第四步-別太早抽-usemcp-框架層" aria-label="Permalink to “第四步：別太早抽 useMcp() 框架層”">&#8203;</a></h3>
<p>WebMCP 還在 Community Group draft，API 形狀、permission model、最佳實踐都可能變動。太早把一套 <code>useMcpComposable()</code> 抽成內部框架，相當於把不穩定 proposal 凍結進 API，之後 spec 改一下整個 codebase 都要連動。</p>
<p>我的判斷標準是「同一段 composable 在第三個專案複製貼上」才考慮抽出來，而且只抽薄框架層（SSR 守衛、生命週期），業務邏輯留 example/recipe，不要進 core。</p>
<hr>
<h2 id="vue-場景的劣勢與務實決策" tabindex="-1">Vue 場景的劣勢與務實決策 <a class="header-anchor" href="#vue-場景的劣勢與務實決策" aria-label="Permalink to “Vue 場景的劣勢與務實決策”">&#8203;</a></h2>
<p>寫到這裡都在講 Vue 的好，接下來換一段誠實的反面。Vue 不是萬能，有些 AX 場景反而是 Vue 的短處。</p>
<h3 id="ssr-hydration-邊界要嚴格守" tabindex="-1">SSR / hydration 邊界要嚴格守 <a class="header-anchor" href="#ssr-hydration-邊界要嚴格守" aria-label="Permalink to “SSR / hydration 邊界要嚴格守”">&#8203;</a></h3>
<p>這個是 Nuxt / VitePress 場景必踩的雷。WebMCP 註冊邏輯如果不小心放在 module top-level、plugin 初始化、setup 同步階段、或 Nuxt 的 universal plugin，server 端執行會直接炸，因為 <code>navigator</code> 不存在。</p>
<p>守則只有一條：<strong>註冊邏輯嚴格放在 client-only lifecycle</strong>。具體做法是 <code>onMounted</code> 內部、<code>if (import.meta.env.SSR) return</code> 守衛、Nuxt 的 <code>.client.ts</code> plugin 命名。只要嚴守 client-only，SSR 風險可控，危險的是不小心放到 universal scope。</p>
<h3 id="tool-boundary-的資料正規化是新手坑" tabindex="-1">Tool boundary 的資料正規化是新手坑 <a class="header-anchor" href="#tool-boundary-的資料正規化是新手坑" aria-label="Permalink to “Tool boundary 的資料正規化是新手坑”">&#8203;</a></h3>
<p>WebMCP 的 tool execute 函式，輸入輸出原則上只該傳 plain JSON-compatible data。但 Vue component 裡的 ref / reactive / computed 是 proxy，如果直接把 reactive 物件回傳給 agent，可能會出現幾種問題：</p>
<ul>
<li>proxy identity 跟 raw identity 不同，跨邊界比對會錯</li>
<li>巢狀 ref / computed 被序列化成意外形狀</li>
<li>class instance、Date、Map、Set、File、Blob、function 跨邊界本來就不安全</li>
<li>tool schema 預期 plain JSON，proxy 物件不一定符合</li>
</ul>
<p>解法是必要時用 <code>toRaw()</code>、<code>unref()</code>、schema parse、<code>structuredClone()</code> 做正規化。Code review 應該把這個列為重點檢查項，因為一旦寫錯，測試在瀏覽器裡跑常常看不出問題，只有 agent 端的回應品質會慢慢崩壞。</p>
<h3 id="component-邊界-tool-邊界" tabindex="-1">Component 邊界 ≠ tool 邊界 <a class="header-anchor" href="#component-邊界-tool-邊界" aria-label="Permalink to “Component 邊界 ≠ tool 邊界”">&#8203;</a></h3>
<p>跨頁流程是 Vue + WebMCP 最不順手的地方。例如電商結帳橫跨「購物車 → 確認地址 → 選付款 → 完成」三四頁，這條 flow 上的 tool 不能放任何單一 component，因為 component 一卸載 tool 就消失。</p>
<p>這時候 tool 必須放 store 或全域 composable，意味著「tool 跟 UI 住在一起」的好處在跨流程場景失效。一個專案會混用兩種風格（component-scoped 跟 store-scoped），一致性會下降，新進工程師需要學會分辨「什麼時候放哪」。</p>
<p>我目前能想到比較順的解法有三條，不能說完美，但都比「亂放」好：</p>
<p><strong>(a) Layout component 當 tool scope</strong>
跨頁 flow 通常會有共用 layout（例如 <code>&lt;CheckoutLayout&gt;</code> 包住結帳的四頁），把 flow 級 tool 的 <code>useMcpTool()</code> 呼叫放在 layout 的 setup 函式裡，讓 layout 的生命週期當 tool scope。使用者進入 <code>/checkout/*</code> 路由就註冊，離開整個 layout 才卸載。這個寫法保留了「tool 跟 UI 住在一起」的好處，又解決 component 一頁一卸載的問題。</p>
<p><strong>(b) 流程級 composable 顯式管理 scope</strong>
如果 flow 由多個獨立 component 組合，沒有共用容器，可以寫一個 flow-level composable，由 flow 入口呼叫、出口呼叫 dispose：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// flows/checkout/useCheckoutFlowTools.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useCheckoutFlowTools</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">typeof</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> navigator </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'undefined'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ||</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'modelContext'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> in</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> navigator)) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ctx</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (navigator </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> any</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).modelContext</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ctx.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">registerTool</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'apply_coupon'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/* ... */</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ctx.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">registerTool</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'place_order'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/* ... */</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> })</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // flow 退出時呼叫，進行清理</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // WebMCP unregister API 規格成熟後接上</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br></div></div><p>把 register / dispose 兩端寫在同一個檔案，code review 比塞在 store 裡更看得出 scope 範圍。要 caveat 的是 WebMCP 規格目前沒明確的 unregister API，dispose 函式現階段比較像佔位，實務上靠 page 卸載清掉註冊狀態，等規格補完才能真正做 cross-flow 清理。</p>
<p><strong>(c) vue-router navigation guard</strong>
最後一條走路由：<code>beforeEach</code> 進入 flow 時呼叫 register，<code>afterEach</code> 偵測離開時呼叫 dispose。邏輯集中在 router 設定檔，所有 flow 級 tool 都在同一個地方管理；缺點是跟 component 距離拉遠，drift 風險回來了。</p>
<p>實務上我會優先試 (a)，layout-driven 既符合 Vue 慣例、又抑制 drift；(b) 留給沒共用 layout 但邊界仍清楚的場景；(c) 通常是 codebase 已經習慣「集中註冊」風格才用。團隊裡訂一條清楚規則（例如「flow 級 tool 一律走 layout，store 只放純資料、不放 tool 註冊」），新進工程師才不會混亂。</p>
<h3 id="bundle-size-對-agent-first-場景不划算" tabindex="-1">Bundle size 對 agent-first 場景不划算 <a class="header-anchor" href="#bundle-size-對-agent-first-場景不划算" aria-label="Permalink to “Bundle size 對 agent-first 場景不划算”">&#8203;</a></h3>
<p>Vue 3 + Pinia + Router + WebMCP runtime 加起來大致落在數十 KB gzip 等級（實際數字以你的 bundle analyzer 為準，受 tree-shaking 跟用量影響）。對一般 SPA 這個 size 沒問題，但對 <strong>agent-first 場景</strong>（API doc、純內部工具、純 agent endpoint）就完全不划算。</p>
<p>agent 不需要 reactivity、不需要 component 樹、不需要 router，給它一套完整 reactive SPA stack 等於 99% 的能力它用不到。這類場景用原生 JS 寫一支百來行的 WebMCP 註冊腳本反而乾淨。</p>
<h3 id="devtools-跨兩個世界" tabindex="-1">Devtools 跨兩個世界 <a class="header-anchor" href="#devtools-跨兩個世界" aria-label="Permalink to “Devtools 跨兩個世界”">&#8203;</a></h3>
<p>Vue Devtools 看不到 MCP 呼叫，MCP Inspector 看不到 reactivity 訊號。沒有單一介面同時看到「tool → action → state → re-render」這條鏈路，debug 的時候要自己埋 trace，在兩個 devtools 之間切換對照。這個目前無解，只能等生態成熟。</p>
<h3 id="決策樹" tabindex="-1">決策樹 <a class="header-anchor" href="#決策樹" aria-label="Permalink to “決策樹”">&#8203;</a></h3>
<p>整理一下什麼場景值得用 Vue 做 WebMCP、什麼不值得：</p>
<table tabindex="0">
<thead>
<tr>
<th>情境</th>
<th>建議</th>
</tr>
</thead>
<tbody>
<tr>
<td>已是 Vue + 重 form/state（後台、CMS、SaaS dashboard）</td>
<td>優勢遠大於劣勢，值得做</td>
</tr>
<tr>
<td>內容驅動、SSR 為主、互動少（部落格、行銷頁、文件站）</td>
<td>Read-only tool 用原生 JS 註冊更乾淨</td>
</tr>
<tr>
<td>全新專案還在選技術棧</td>
<td>不要為了 WebMCP 選 Vue。技術棧由團隊熟悉度跟主要使用者決定，agent 整合放第二順位</td>
</tr>
</tbody>
</table>
<p>順帶補一個常見誤解。對純內容站（部落格、行銷頁、文件站）而言，先把 semantic HTML、結構化資料（Schema.org）、清楚的標題層級整理乾淨，對 AI crawler 跟 Browser Agent 的幫助比直接做 WebMCP 還大。WebMCP 走的是 <code>navigator.modelContext.registerTool()</code>，繞過 DOM；做語意化 HTML 服務的是另一群讀 DOM 的 AI 訪客（搜尋爬蟲、Computer Use 那類）。沒做 WebMCP 不等於對 AI 擺爛，看你想接的是哪種訪客。</p>
<hr>
<h2 id="動工前該寫的兩份清單" tabindex="-1">動工前該寫的兩份清單 <a class="header-anchor" href="#動工前該寫的兩份清單" aria-label="Permalink to “動工前該寫的兩份清單”">&#8203;</a></h2>
<p>最後是我覺得對成熟網站引入 WebMCP 最關鍵的一個動作：在 sprint kickoff 前，跟團隊一起花一個下午寫兩份清單。這兩份清單會直接決定後續排程的優先順序，也避免做著做著，一個寫入類 tool 偷偷溜上 production。</p>
<h3 id="清單一-現有-api-哪些可安全包成-tool" tabindex="-1">清單一：現有 API 哪些可安全包成 tool <a class="header-anchor" href="#清單一-現有-api-哪些可安全包成-tool" aria-label="Permalink to “清單一：現有 API 哪些可安全包成 tool”">&#8203;</a></h3>
<p>把站上所有 API endpoint 列出來，對每一支問三個問題：</p>
<table tabindex="0">
<thead>
<tr>
<th>檢查項</th>
<th>問題</th>
<th>通過標準</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>可呼叫性</strong></td>
<td>這支 API 可不可以無人類確認直接呼叫？</td>
<td>純查詢、無副作用 → 通過</td>
</tr>
<tr>
<td><strong>Schema 完備性</strong></td>
<td>輸入是不是已經有清楚的 schema？</td>
<td>已有 Zod / OpenAPI / JSON Schema 定義 → 通過</td>
</tr>
<tr>
<td><strong>回傳結構性</strong></td>
<td>回傳格式是不是 agent 友善？</td>
<td>結構化 JSON、錯誤碼可區分 → 通過</td>
</tr>
</tbody>
</table>
<p>三題都通過的優先做，可以包成 read-only tool 馬上上線；通過兩題的次之，補一補就能上；只通過一題或全敗的最後做，可能要先改 API 才包得起來。這個分級會直接餵給 sprint planning 的 backlog，避免「想到什麼就先做什麼」的隨機開發節奏。</p>
<h3 id="清單二-哪些操作絕對不開放給-agent" tabindex="-1">清單二：哪些操作絕對不開放給 agent <a class="header-anchor" href="#清單二-哪些操作絕對不開放給-agent" aria-label="Permalink to “清單二：哪些操作絕對不開放給 agent”">&#8203;</a></h3>
<p>這份清單可能比清單一還重要。常見的紅線項目大概可以分成五類：</p>
<ul>
<li><strong>不可逆動作</strong>：刪除使用者、刪除訂單、清空購物車、移除上傳檔案</li>
<li><strong>金流相關</strong>：變更付款資訊、轉帳、退款核可、修改訂閱方案</li>
<li><strong>權限變更</strong>：升降級角色、加減團隊成員、產生 API key、變更 SSO 設定</li>
<li><strong>對外通訊</strong>：寄客訴信、發出工單、推播通知、群發 email</li>
<li><strong>法規敏感</strong>：個資存取、醫療紀錄、未成年資訊、跨境資料傳輸</li>
</ul>
<p>這些動作就算上 <code>requestUserInteraction</code> 二次確認都未必夠，因為 agent 可以在前面的對話誘導使用者「等下會跳出確認框，按確認就好」。
安全做法是直接從 tool 列表排除，只留人類走 UI，agent 想做就主動跟使用者說「這個我不能幫你，請你自己操作」。</p>
<h3 id="兩份清單怎麼用" tabindex="-1">兩份清單怎麼用 <a class="header-anchor" href="#兩份清單怎麼用" aria-label="Permalink to “兩份清單怎麼用”">&#8203;</a></h3>
<p>寫完之後，把清單一的優先順序帶進 sprint planning，把清單二的紅線寫進 code review checklist（PR 加 tool 之前，reviewer 先檢查 tool 名稱有沒有撞到紅線）。
每個 sprint 結束更新一次：新的 API 上線就追加到清單一，業務變化就調整紅線範圍。清單寫完歸檔不更新，等於沒寫。</p>
<hr>
<h2 id="後記" tabindex="-1">後記 <a class="header-anchor" href="#後記" aria-label="Permalink to “後記”">&#8203;</a></h2>
<p>寫到這裡，我自己對 AX 這件事的感覺是，它不會在一兩個月內顛覆前端工作，但會在一兩年內慢慢滲透進每個 Vue 開發者的日常。Pinia action 多寫兩行 description、表單多走一次 Zod schema、router meta 多塞一個 <code>agentAccessible</code> flag，這些小動作累積起來，就是這個世代前端工程師的新基本功。</p>
<p>身為 Vue 的開發者，我認為 Vue.js 在這場轉型裡的位置是有利的。
Vue reactivity 對「state 同時餵 UI 跟 agent」近乎零成本，Pinia 對 tool 註冊起手式接近終點，SFC 對 drift 抑制天然有效。
這些都是 mental model 上的契合，不是套件數量的堆疊。</p>
<p>寫這篇的同時，好友 Paul Li 拿 Yahoo!拍賣的頁面做了一個 WebMCP demo。他註冊了兩個 tool，<a href="https://www.facebook.com/reel/828135353702068" target="_blank" rel="noreferrer">demo 影片</a>裡示範 agent 辨識「找預算內的 Nintendo Switch 2」這類使用者意圖時，會直接呼叫對應 tool 跳到最精準的搜尋結果頁，跳過視覺辨識跟 DOM 解析那套。</p>
<p>雖然這還只是 demo 不是上線版本，但能在大型電商規模的頁面上跑出兩個 tool 的差異就已經很有意義，至少能說明動手實驗的成本沒大家想像中高。</p>
<p>至於 WebMCP 規格本身會不會像 Schema.org / microformats 那樣熱鬧開場、最後普及不如預期？老實說我也不知道。
但 Vue.js 的開發生態圈上，做 AX 的邊際成本不高、回頭成本也不高，先從 read-only tool 開始試水溫，觀察一陣子再加碼，我覺得是合理的賭注策略。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue</category>
            <category>webmcp</category>
            <category>ai</category>
            <category>agents</category>
            <category>ax</category>
            <category>frontend</category>
        </item>
        <item>
            <title><![CDATA[走過一輪 Potter Kata：把 TDD 當成一套思考順序]]></title>
            <link>https://kurohsu.dev/learn/learning-tdd-potter-kata.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/learn/learning-tdd-potter-kata.html</guid>
            <pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="走過一輪-potter-kata-把-tdd-當成一套思考順序" tabindex="-1">走過一輪 Potter Kata：把 TDD 當成一套思考順序 <a class="header-anchor" href="#走過一輪-potter-kata-把-tdd-當成一套思考順序" aria-label="Permalink to “走過一輪 Potter Kata：把 TDD 當成一套思考順序”">&#8203;</a></h1>
<p>週末去五倍上了 Cash 哥的 <a href="https://5xcampus.com/courses/tdd" target="_blank" rel="noreferrer">Classic TDD Workshop</a>，重新走了一輪經典的 Potter TDD Kata。</p>
<p>雖然多年前我也上過 91 哥的 TDD 課程，但這次跟著 Cash 哥的節奏重做一次，
除了複習 TDD 的基本精神與流程之外，最大的收穫重新理清 TDD 在開發時的思考順序，以及現今 AI 時代下 TDD 帶來的價值。</p>
<p>Kata 題目本身不難，這篇文章想把 <strong>Red、Green、Refactor</strong> 完整走過一遍之後，記錄一下 TDD 在我腦中留下了什麼。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="走過一輪-potter-kata-把-tdd-當成一套思考順序" tabindex="-1">走過一輪 Potter Kata：把 TDD 當成一套思考順序 <a class="header-anchor" href="#走過一輪-potter-kata-把-tdd-當成一套思考順序" aria-label="Permalink to “走過一輪 Potter Kata：把 TDD 當成一套思考順序”">&#8203;</a></h1>
<p>週末去五倍上了 Cash 哥的 <a href="https://5xcampus.com/courses/tdd" target="_blank" rel="noreferrer">Classic TDD Workshop</a>，重新走了一輪經典的 Potter TDD Kata。</p>
<p>雖然多年前我也上過 91 哥的 TDD 課程，但這次跟著 Cash 哥的節奏重做一次，
除了複習 TDD 的基本精神與流程之外，最大的收穫重新理清 TDD 在開發時的思考順序，以及現今 AI 時代下 TDD 帶來的價值。</p>
<p>Kata 題目本身不難，這篇文章想把 <strong>Red、Green、Refactor</strong> 完整走過一遍之後，記錄一下 TDD 在我腦中留下了什麼。</p>
<hr>
<p><img src="/images/learn/tdd-potter-kata-cover.jpg" alt="暖色調深色木質書房裡懸浮著一個由紅色、綠色、琥珀金三段箭頭組成的循環環，每一段中央分別有一顆對應顏色的發光圓點，象徵 TDD 紅燈 → 綠燈 → 重構不斷循環的節奏；背景有書架、檯燈、植栽、馬克杯等幾何化的書房元素"></p>
<p>題目的需求很單純：哈利波特一套五本，單本 100 元，2 本不同 95 折、3 本 9 折、4 本 8 折、5 本 75 折。要實作 <code>add(cart, book)</code> 跟 <code>calculatePrice(cart)</code> 兩個 function：<code>add</code> 把書加進購物車並回傳新的 cart，<code>calculatePrice</code> 根據購物車的內容算出最便宜的總價。</p>
<h2 id="不只是「先寫測試」" tabindex="-1">不只是「先寫測試」 <a class="header-anchor" href="#不只是「先寫測試」" aria-label="Permalink to “不只是「先寫測試」”">&#8203;</a></h2>
<p>如果是不理解 TDD 的朋友，我想大家對 TDD 的印象可能大多停在表面的「先寫測試」。
所以我覺得如果只把 Red-Green-Refactor 三步當流程記下來，這篇筆記就很容易流於表面。</p>
<p>但 TDD 真正的價值，是它逼出一個開發時的思考順序。「<strong>TDD 的順序 = 你對問題的理解順序。</strong>」測試先寫成什麼樣，反映的就是你對需求理解到什麼程度。</p>
<h3 id="_1-先回答需求是什麼" tabindex="-1">1. 先回答需求是什麼 <a class="header-anchor" href="#_1-先回答需求是什麼" aria-label="Permalink to “1. 先回答需求是什麼”">&#8203;</a></h3>
<p>Potter Kata 表面在算價格，但需求其實有兩層：購物車要能加入書，價格計算要符合折扣規則，而且要找出最便宜的分組方式。這兩層如果沒先拆開，很容易把題目看成「套折扣公式」，最後寫出一個只能處理單純情境的版本。</p>
<p>所以 TDD 第一個帶來的價值，其實是逼自己先回答：這題到底在解什麼問題？哪些行為是外部真的看得見的？哪些只是我腦中以為合理、但需求其實沒有保證的事？</p>
<h3 id="_2-把抽象需求拆成可驗證的例子" tabindex="-1">2. 把抽象需求拆成可驗證的例子 <a class="header-anchor" href="#_2-把抽象需求拆成可驗證的例子" aria-label="Permalink to “2. 把抽象需求拆成可驗證的例子”">&#8203;</a></h3>
<p>需求一抽象就容易漏邊界。第二步先別急著寫程式，把情境列出來：空購物車怎麼算、一本書怎麼算、多本不同書怎麼套折扣、有重複書怎麼拆組、哪些案例會揭露錯誤的演算法。情境列完之後，這份清單本身就是需求，測試只是把它寫成跑得動的形式。</p>
<h3 id="_3-測試的順序本身在推動設計" tabindex="-1">3. 測試的順序本身在推動設計 <a class="header-anchor" href="#_3-測試的順序本身在推動設計" aria-label="Permalink to “3. 測試的順序本身在推動設計”">&#8203;</a></h3>
<p>Cash 的 commit 順序看起來瑣碎，但每一個 [red] 都只往前推一小步。先測 1 本書、再測 2 本同書、再測 2 本不同、3 本不同、4 本不同、5 本不同，最後才放重複書的 case。這樣排的好處是，每一次失敗都只告訴你一個新的訊息，不會一次把整題複雜度全部灌進來。</p>
<p>也呼應 TDD 一個我覺得很重要的精神：把問題切到夠小，小到每次只需要做一個決定。</p>
<h3 id="_4-先寫失敗的測試-再寫剛好通過的程式" tabindex="-1">4. 先寫失敗的測試，再寫剛好通過的程式 <a class="header-anchor" href="#_4-先寫失敗的測試-再寫剛好通過的程式" aria-label="Permalink to “4. 先寫失敗的測試，再寫剛好通過的程式”">&#8203;</a></h3>
<p>「先寫測試」這句話的重點我認為是控制開發的衝動。</p>
<p>如果先寫程式碼，很容易直接跳進自己熟悉的解法，事後再補測試替它背書。
但如果先讓測試失敗，思考重點會變成：我現在要保證的是哪個行為？我希望下一個失敗訊息告訴我什麼？我是不是正在為還沒被要求的東西提早設計？</p>
<p>而「剛好通過」是另一道煞車。它在提醒自己不要超前設計：忍住不要一次把所有情境都寫完，忍住不要在還沒需要時就先抽象化，忍住不要把重構和解題混在同一步。</p>
<p>Cash 在課堂上講得更直接：「學會忍住，工程師的通病就是會忍不住一口氣做完，剛開始學 TDD 的人會很不習慣，因為太慢了。」TDD 的節奏感很大一部分就是來自這種克制。</p>
<p>看起來慢，其實正是 TDD 的設計。每一步只走一格雖然看起來很瑣碎，但換來的是任何一刻測試紅燈都能精準回退。把這個工程師通病先壓下去，這套節奏才跑得起來。</p>
<h2 id="跟著-cash-的步驟走一遍" tabindex="-1">跟著 Cash 的步驟走一遍 <a class="header-anchor" href="#跟著-cash-的步驟走一遍" aria-label="Permalink to “跟著 Cash 的步驟走一遍”">&#8203;</a></h2>
<p>Cash 哥在課堂上提供的示範 repo 從 init 到完成總共 80 多個 commit，
每一個都標 <code>[red]</code>、<code>[green]</code> 或 <code>[refactor]</code>，每一個都只動很少的程式碼。</p>
<p>如果是不熟 TDD 的朋友，第一次看這串 commit 應該會覺得：「這也太瑣碎了吧。」
但自己跟著重做一次之後，就會懂這個粒度的意義。</p>
<p>而且 Cash 在課堂上特別強調過：每一個紅燈、綠燈、重構都應該各自 commit。除了版本管控的好處之外，更實際的影響是每一步變更都被獨立記錄下來，出問題時可以乾淨地回到上一個綠燈，重構壞掉時也能精準看出是哪一個小步驟動到了行為。這在團隊協作時會更明顯，其他人從 commit 流就能讀出你解題的節奏，而不是只看到一個塞滿邏輯的大 PR。</p>
<p>這個習慣聽起來很瑣碎，但走完一輪就會發現它跟 TDD 的安全感是綁在一起的。</p>
<h3 id="起手式-fake-it" tabindex="-1">起手式：Fake It <a class="header-anchor" href="#起手式-fake-it" aria-label="Permalink to “起手式：Fake It”">&#8203;</a></h3>
<p>第一個 [red] 時，<code>calculatePrice</code> 還只是空殼，<code>add</code> 則先用最直接的 immutable 寫法回傳新購物車：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> add</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">cart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">book</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">cart, book];</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculatePrice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">cart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><p>對應的測試是「一本書應該回傳 100」。Red 之後緊接著 [green]，但這一步沒去湊真正的計算邏輯，只把 return 改成：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculatePrice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">cart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>這就是 Kent Beck 講的 <strong>Fake It</strong>：第一次綠燈不需要通用，只需要通過當前測試。對我這種拿到題目就想直接寫對的人來說，這一步特別有教育意義，它逼你先確認「測試真的能抓到正確答案」、「pipeline 真的能跑完一輪」，這兩件事比演算法早。接著才是 refactor，把 100 抽成 <code>BOOK_PRICE</code> 常數。</p>
<p>一個 commit 一件事，乾淨俐落。</p>
<h3 id="triangulation-用第二個案例逼出泛化" tabindex="-1">Triangulation：用第二個案例逼出泛化 <a class="header-anchor" href="#triangulation-用第二個案例逼出泛化" aria-label="Permalink to “Triangulation：用第二個案例逼出泛化”">&#8203;</a></h3>
<p>下一個 [red] 加了「同一本書買兩次應該回 200」的測試。[green] 是這樣的：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculatePrice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">cart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (cart.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ===</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> BOOK_PRICE</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p>還是 hardcode，但已經出現分支。緊接著的 refactor 才是亮點，分兩步走：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// step 1: [refactor] - duplicate code</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (cart.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ===</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> BOOK_PRICE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> BOOK_PRICE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// step 2: [refactor] - remove duplicate</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> BOOK_PRICE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>第一步先讓兩個分支「故意長得一樣」，第二步才把 if 砍掉。這個技巧叫 <strong>duplicate-then-remove</strong>，看起來多此一舉，但好處是每個 commit 都維持綠燈。如果直接把 if 跟分支內容一起改，重構過程中萬一壞掉，就找不出是哪一步出的事。</p>
<p>這就是 Triangulation 的精神：用兩個案例同時夾住正確答案，把 hardcode 的個案逼成通用解。</p>
<p>原本我習慣的是「想出通用解再寫」，TDD 想要的是「先寫個案、再被測試逼出通用解」。</p>
<h3 id="從-if-鏈演化成-lookup-table" tabindex="-1">從 if 鏈演化成 lookup table <a class="header-anchor" href="#從-if-鏈演化成-lookup-table" aria-label="Permalink to “從 if 鏈演化成 lookup table”">&#8203;</a></h3>
<p>到了三本不同書 (<code>price 1, 2, 3</code>)，[green] 又是一個分支：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (cart[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> BOOK_PRICE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0.9</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (cart[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)                  </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> BOOK_PRICE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0.95</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> BOOK_PRICE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>接下來 7 個 refactor commit，每一個都只改一點點，逐步把 if 鏈轉成 <code>discountLookup</code> 對照表 + <code>new Set(cart).size</code> 的查表寫法：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> discountLookup</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.95</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.9</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> distinctBooks</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Set</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(cart);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> BOOK_PRICE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> discountLookup[distinctBooks.size];</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>到這一步之後，4 本和 5 本的 case 只要在 <code>discountLookup</code> 多加兩行就直接綠燈，連 <code>calculatePrice</code> 本體都不用改。 如果不是用 TDD 的方式，我可能會直接就寫成 lookup 形式，但沒體會到的差別是：這個 lookup 是被多個 case 慢慢逼出來的形狀，先有需求，才有對應的結構。</p>
<p><img src="/images/learn/IMG_5579.jpeg" alt="Cash: 想一下每一個重構的流程，為什麼要這樣做、重構的順序很重要。"></p>
<p>Cash 在這段特別提醒過一句我印象很深的話：「想一下每一個重構的流程，為什麼要這樣做、重構的順序很重要。」</p>
<p>這幾個 commit 的順序回頭看完全不是隨手排的：先把 <code>discountLookup</code> 跟原本的 if 分支並存放著，再一個分支一個分支地改用 lookup，接著導入 <code>new Set(cart)</code> 把判斷依據從 hardcode 的 index 換成 <code>distinctBooks.size</code>，最後把 lookup 補齊到 1 本書的分支也能套用，這時候三個 if 分支才終於長得一模一樣，砍 if 才安全。</p>
<p>如果中間隨便調換一步，例如先用 <code>distinctBooks.size</code> 當查表 key、但 lookup 裡還沒有對應的條目，馬上就會踩到 <code>undefined</code> 把測試打紅。</p>
<p>重構真正在做的事情是「在保持綠燈的前提下、一步一格把結構往前推」，至於程式變不變漂亮，是順帶的結果，這個前提決定了你能用什麼順序。</p>
<h3 id="卡關時的勇氣-suspend-test" tabindex="-1">卡關時的勇氣：Suspend Test <a class="header-anchor" href="#卡關時的勇氣-suspend-test" aria-label="Permalink to “卡關時的勇氣：Suspend Test”">&#8203;</a></h3>
<p>接下來 <code>[red] price 1, 2, 1</code>（買兩次第 1 本加一次第 2 本，期望 290）就把整個 distinct-set 解法打爆了。distinctSet.size 是 2，套錯折扣，得到 285，差 5 元。</p>
<p>這時候很多人（包括我自己）會直接動 <code>calculatePrice</code>，硬塞分支去通過測試。但 Cash 做的事是 <code>[red] price 1, 2, 1 - comment</code>，把這個測試<strong>先註解起來</strong>。</p>
<p>這個動作叫 <strong>Suspend Test</strong>。如果當前測試對現在的程式碼來說「跨度太大」，與其硬塞，不如把它擱著，drill down 去開更小的 helper 函式，等基礎建好再回來。這個動作的核心是承認自己現在還沒有足夠的工具，先去打造工具、再回頭解這個 case。</p>
<h3 id="drill-down-用-tdd-開-helper-函式" tabindex="-1">Drill Down：用 TDD 開 helper 函式 <a class="header-anchor" href="#drill-down-用-tdd-開-helper-函式" aria-label="Permalink to “Drill Down：用 TDD 開 helper 函式”">&#8203;</a></h3>
<p>接下來 30 多個 commit 全部在開兩個輔助函式：<code>countBooks</code> 跟 <code>groupBook</code>，而且每個 helper 自己也是完整的 red-green-refactor 循環。</p>
<p><code>countBooks</code> 從 <code>{1:1}</code> 的 hardcode 開始，經過 fake it、用 <code>cart[0]</code>、改 <code>for of</code>、加分支處理重複，最後演化成：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> countBooks</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">cart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> counts</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {};</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> book</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> of</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        counts[book] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (counts[book] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> counts;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><p><code>countBooks</code> 解的是「每一本出現幾次」，<code>groupBook</code> 解的是「根據這些次數，先切出一組組可計價的書」。前者把資料整理成可推理的形狀，後者才開始處理折扣分組。</p>
<p><code>groupBook</code> 也是一樣，從 <code>[[1]]</code> 的 hardcode 開始，逐步加進「拿 distinct 書」、「按最大重複數開 N 組」、「把每本書平均分配到各組」，演化成：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> groupBook</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">cart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> counts</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> countBooks</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(cart);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> volumes</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Object.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">keys</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(counts);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> groups</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [];</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> maxGroupCount</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">max</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Object.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">values</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(counts));</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> maxGroupCount; i</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">++</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) groups.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">push</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([]);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> volume</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> of</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> volumes) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> counts[volume]; i</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">++</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            groups[i].</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">push</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">volume);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> groups;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br></div></div><p>這個分組策略目前的巧妙之處，是它直接照「最多重複幾本書」開 N 組，再把每本書水平攤到各組。<code>[1,2,1,2]</code> 自然變成 <code>[[1,2],[1,2]]</code>，每組都吃滿 5% 折扣。沒有窮舉、沒有遞迴，純粹是被一連串小 case 逼出來的形狀。不過這個策略還不是 Potter Kata 的終點，後面有更難的最佳分組案例會再暴露它的限制。</p>
<h3 id="回到主測試" tabindex="-1">回到主測試 <a class="header-anchor" href="#回到主測試" aria-label="Permalink to “回到主測試”">&#8203;</a></h3>
<p>helper 都建好之後，<code>[red] price 1, 2, 1 - uncomment</code> 把原本擱著的測試恢復，<code>[green]</code> 用 <code>groupBook</code> 重寫 <code>calculatePrice</code>：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculatePrice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">cart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> groups</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> groupBook</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(cart);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> price </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> group</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> of</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> groups) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        price </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> BOOK_PRICE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> group.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> discountLookup[group.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> price;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>到這裡所有測試都通過。再加一個 <code>[1,2,1,2]</code> 的 case，發現不用動任何程式碼也直接綠燈，演化出來的解法天然支援它。這個瞬間我覺得是最爽的：你會發現自己一路小步走下來的程式，已經默默支援了新的需求，根本沒有刻意去「設計」可重用性。</p>
<p>「<strong>TDD 是浮現設計（Emergent Design）的方式，讓設計從解決問題的過程中浮現出來。</strong>」一路上每個小決定都只解決當下的測試，但這些決定累加起來，就是後面那個剛好接得住新 case 的形狀。</p>
<h2 id="從這趟重做學到的-tdd-技法" tabindex="-1">從這趟重做學到的 TDD 技法 <a class="header-anchor" href="#從這趟重做學到的-tdd-技法" aria-label="Permalink to “從這趟重做學到的 TDD 技法”">&#8203;</a></h2>
<p>回頭整理 Cash 示範用到的招式，每一個或許我們在 Kent Beck 的《TDD by Example》書裡都聽過名字，
但真正走過一遍之後，才會體會到它們在實際操作中的意義。</p>
<p><strong>Fake It Till You Make It</strong>。第一次綠燈直接 hardcode，先確認測試 pipeline 跑得起來，演算法之後再說。</p>
<p><strong>Triangulation</strong>。等第二、第三個 case 出現，才把 hardcode 推往泛化。設計是被一個個 case 逼出來的形狀，自己事先想破頭也想不到那麼貼切。</p>
<p><strong>Duplicate-then-Remove</strong>。要刪 if 分支前，先把每個分支變成一樣的程式碼，再砍 if。每個中間 commit 都還能跑，壞掉就回得去。</p>
<p><strong>Suspend Test</strong>。當前 case 對現在的程式碼跨度太大，先註解起來，drill down 開更小的 helper。擱著聽起來像放棄，其實是承認手上還缺工具，先去打造它。</p>
<p><strong>Child Test</strong>。被 suspend 的測試底下，每個 helper 函式自己也跑完整的 red-green-refactor。整個過程是巢狀展開的 TDD，會在 helper 裡再開一輪完整循環。</p>
<p><strong>Merciless Refactoring</strong>。每個小重構都是獨立 commit，連抽變數、改 for 迴圈都各自一筆。不混在實作裡，因為混了之後就分不清哪一步壞掉。而且每一步的順序本身就是設計，隨手換個順序，中間某一步就會打破綠燈，安全網就破了。</p>
<h2 id="延伸的挑戰-最佳分組的-case" tabindex="-1">延伸的挑戰：最佳分組的 case <a class="header-anchor" href="#延伸的挑戰-最佳分組的-case" aria-label="Permalink to “延伸的挑戰：最佳分組的 case”">&#8203;</a></h2>
<p>老實說，Cash 示範的 commit 序列收在 <code>[1,2,1,2] → 380</code> 這個 case，演化出來的「按最大重複數平均拆組」解法剛好能處理它。</p>
<p>但我自己在練習時其實還加了一個更難的 case: <code>['A','A','B','B','C','C','D','E'] → 640</code> ，結果發現這才是真正麻煩的地方。 XD</p>
<p>這個 case 會打爆任何「平均拆組」的策略，因為最佳解是 <code>ABCD + ABCE</code>（兩組四本不同 = 640）而不是 <code>ABCDE + ABC</code>（五本加三本 = 645）。</p>
<p>Cash 在課堂上提過一句很關鍵的話：「<strong>你的演算法會影響到你對 TDD 的設計，因為你的每一步都很小，所以當你發現改不下去的時候，就應該要停下來調整。</strong>」面對 640 這個 case，「平均拆組」就是改不下去的時刻，這個訊號要說的是該回頭重新看演算法，硬撐著繼續塞分支只會越改越亂。</p>
<p>我自己第一次寫的時候是直接跳到 bitmask 窮舉 + memoization 把它一次解掉。事後想其實方向是對的（真的需要換演算法），但跳得太大，整個 TDD 的演化過程被自己折掉了。</p>
<p>比較理想的做法是把這個 case suspend 起來，drill down 到一個更通用的 split 函式，用 baby steps 把它推回去。不然就算最後寫出來了，過程中也會一直打紅燈，完全失去 TDD 的安全感。</p>
<p>回頭看這整段卡關，「<strong>TDD 可以幫你發現問題，但不完全幫你解決演算法。</strong>」紅燈會告訴你「現在的解法不夠用」，但要換成什麼演算法、要怎麼換，這一步還是得靠開發者自己想。</p>
<h2 id="tdd-的節奏感" tabindex="-1">TDD 的節奏感 <a class="header-anchor" href="#tdd-的節奏感" aria-label="Permalink to “TDD 的節奏感”">&#8203;</a></h2>
<p>做完這次練習之後，比起記住術語，我更想留住的是這個順序，或者說節奏感：<strong>先用紅燈建立規格，再用綠燈完成行為，最後靠測試保護重構</strong>，而且這三步是「每一個 case」都跑一輪，不是整個專案只跑一輪。</p>
<p>對應到實際操作的順序大概是這樣：先搞懂需求、不急著寫解法；先把需求拆成情境、不急著寫完整架構；先安排測試順序、讓每一步只增加一點複雜度；先讓測試失敗、確認真的有抓到缺口；先寫剛好通過的程式、不偷跑；最後才重構，讓測試當安全網。</p>
<p>平常自己寫程式時，這套順序最容易破功的地方就是「忍住不超前」這一步。</p>
<p>常常是寫一寫覺得「反正等下也要寫到」就提前抽象、提前寫成通用解，最後測試已經被實作牽著走，而不是反過來規範實作。Potter Kata 是個剛剛好的尺寸，可以把這個順序硬性走完一遍，提醒自己：先 TDD 才能把這題拆得簡單，題目本身並沒有比較簡單。</p>
<h2 id="tdd-的價值-讓-ai-快跑的安全網" tabindex="-1">TDD 的價值：讓 AI 快跑的安全網 <a class="header-anchor" href="#tdd-的價值-讓-ai-快跑的安全網" aria-label="Permalink to “TDD 的價值：讓 AI 快跑的安全網”">&#8203;</a></h2>
<p>雖然現在已經是 AI Coding 時代，自己一行一行手刻 code 的機會其實越來越少。但走完這趟之後我反而更覺得，TDD 真正留下的東西，是它逼你在動手前先把需求、行為、邊界想清楚的那套思考順序。「寫測試」這個動作本身只是表象。</p>
<p>當 AI 寫 code 的速度快到一個程度，「先寫測試」反而從手刻時代被嫌慢的瓶頸，變成了唯一還跟得上 AI 速度的 spec 形式。
手刻時代沒先寫測試，頂多事後補救比較痛；AI 時代沒先寫測試，你根本不知道 AI 生出來的那一坨東西到底對不對。</p>
<p>以前 TDD 那種「忍住不超前」的克制看起來像在拖慢進度，但到了 AI 時代它反而成了讓 AI 可以放心快跑的安全網。</p>
<p>而最近也常被拿來討論的 SDD（Spec-Driven Development）剛好補上另一塊：先把需求寫成結構化的 spec 文件，AI 再照著 spec 生 code。 SDD 把人腦中模糊的需求外化成 AI 讀得懂的文字，TDD 則把同一份意圖翻譯成可執行的測試邊界。一個給 AI 看「要做什麼」，一個在跑完後驗證「有沒有做對」。</p>
<p><strong>SDD 定義任務需求，TDD 用測試框住行為邊界，AI 負責在這個範圍內填滿實作。</strong>
三件事各司其職，1 + 1 才能大於 2。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>tdd</category>
            <category>testing</category>
            <category>vitest</category>
            <category>javascript</category>
            <category>kata</category>
        </item>
        <item>
            <title><![CDATA[讓網站直接跟 AI Agent 對話：初試 WebMCP]]></title>
            <link>https://kurohsu.dev/learn/learning-webmcp.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/learn/learning-webmcp.html</guid>
            <pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="讓網站直接跟-ai-agent-對話-初試-webmcp" tabindex="-1">讓網站直接跟 AI Agent 對話：初試 WebMCP <a class="header-anchor" href="#讓網站直接跟-ai-agent-對話-初試-webmcp" aria-label="Permalink to “讓網站直接跟 AI Agent 對話：初試 WebMCP”">&#8203;</a></h1>
<p>WebMCP 是讓網站直接把功能暴露給 AI Agent 的新 Web 平台 API，由 Google 跟 Microsoft 主導，目前還在 W3C Web Machine Learning Community Group 的草案階段。核心 API 是 <code>navigator.modelContext.registerTool()</code>，網站把可呼叫的 JavaScript function 註冊上去，Agent 就能直接照 schema 呼叫，省掉「截圖認 UI 推按鈕」那一套。</p>
<p>前陣子做 <a href="/notes/making-vitepress-blog-agent-ready.html">Agent Ready 改造</a>時，這項我刻意沒做，理由是 Chrome 還在 Early Preview、spec 也還在改、對靜態網站 cp 值不對等。 這幾天花點時間把官方文件、幾份訪談、社群 polyfill 都翻過一輪，也動手寫了一個小 demo 實際跑過，整理成這篇學習筆記。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="讓網站直接跟-ai-agent-對話-初試-webmcp" tabindex="-1">讓網站直接跟 AI Agent 對話：初試 WebMCP <a class="header-anchor" href="#讓網站直接跟-ai-agent-對話-初試-webmcp" aria-label="Permalink to “讓網站直接跟 AI Agent 對話：初試 WebMCP”">&#8203;</a></h1>
<p>WebMCP 是讓網站直接把功能暴露給 AI Agent 的新 Web 平台 API，由 Google 跟 Microsoft 主導，目前還在 W3C Web Machine Learning Community Group 的草案階段。核心 API 是 <code>navigator.modelContext.registerTool()</code>，網站把可呼叫的 JavaScript function 註冊上去，Agent 就能直接照 schema 呼叫，省掉「截圖認 UI 推按鈕」那一套。</p>
<p>前陣子做 <a href="/notes/making-vitepress-blog-agent-ready.html">Agent Ready 改造</a>時，這項我刻意沒做，理由是 Chrome 還在 Early Preview、spec 也還在改、對靜態網站 cp 值不對等。 這幾天花點時間把官方文件、幾份訪談、社群 polyfill 都翻過一輪，也動手寫了一個小 demo 實際跑過，整理成這篇學習筆記。</p>
<hr>
<p><img src="/images/learn/webmcp-cover.jpg" alt="暖色調深色底的示意圖：中央是一個電商網站的商品列表頁面，UI 元件旁邊用手寫風便利貼標示對應的 WebMCP tool 呼叫，例如 search_products、filter_results、add_to_cart、checkout 等；左側一隻幾何化的手從畫面外伸入，正在點選「Add to cart」按鈕，代表 Agent 透過 structured tool call 直接操作網站"></p>
<p>Demo 單獨放在 <a href="https://github.com/kurotanshi/kuro-roasters-webmcp" target="_blank" rel="noreferrer">kuro-roasters-webmcp</a> 那個 repo 了，這裡聚焦記錄一下 WebMCP 大致的介紹，以及開發者會踩到什麼坑、我猜想的未來可能的發展方向等等。</p>
<h2 id="為什麼要有-webmcp" tabindex="-1">為什麼要有 WebMCP <a class="header-anchor" href="#為什麼要有-webmcp" aria-label="Permalink to “為什麼要有 WebMCP”">&#8203;</a></h2>
<p>AI Agent 要操作網頁目前有兩條路。</p>
<p>一條是後端 API 或 MCP server，優點是穩、可控，但前提是要看網站有開放。
多數站台沒有，有的話還要處理 OAuth、API key、rate limit 這些東西。</p>
<p>另一條是現在最常見的 Browser Agent（Claude Computer Use、OpenAI Operator 那類），讓模型看畫面、認 UI、決定要點哪個按鈕；不用網站配合，但每一步都要塞截圖或整份 DOM 進 context，又慢又貴、網站改個 class 名稱就壞。</p>
<p>WebMCP 切第三條路：<strong>讓網站自己把能做的事用 Agent 看得懂的結構講清楚</strong>。與其讓模型從畫面去猜，不如網站主動宣告一組 JavaScript function，每個都有名字、自然語言描述、input schema，Agent 直接照 schema 呼叫就好。<a href="https://www.scalekit.com/blog/webmcp-the-missing-bridge-between-ai-agents-and-the-web" target="_blank" rel="noreferrer">Scalekit 的 explainer</a> 裡有個挺具體的對比：「新增一個叫 Drugstore 的分店，然後加一支護唇膏」這種任務，傳統 Browser Agent 要 30 到 60 秒，走 WebMCP tool call 大約 5 秒搞定。差別不只是快慢，是一個要截十幾張圖、一個是兩次 function call 就完事。</p>
<p>WebMCP 目前是 W3C Web Machine Learning Community Group 孵化中的 Draft Community Group Report（寫這篇時草案版本是 2026-04-23，還在迭代），明確不是 W3C Standard、也不在 Standards Track，Google 跟 Microsoft 主導推進。<a href="https://developer.chrome.com/blog/webmcp-epp" target="_blank" rel="noreferrer">Chrome 146 Canary</a> 起開放 Early Preview Program，其他瀏覽器還沒原生支援，但 Mozilla 跟 Apple 的工程師都有參與 working group，不是完全缺席。</p>
<h2 id="webmcp-怎麼用" tabindex="-1">WebMCP 怎麼用？ <a class="header-anchor" href="#webmcp-怎麼用" aria-label="Permalink to “WebMCP 怎麼用？”">&#8203;</a></h2>
<p>核心 API 就一個入口：<code>navigator.modelContext.registerTool()</code>。一個 tool 的註冊長這樣：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'modelContext'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> in</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> navigator) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  navigator.modelContext.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">registerTool</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'search_products'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    description: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'搜尋商品。支援關鍵字、價格上限過濾。'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    inputSchema: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'object'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      properties: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        query:    { type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'string'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, description: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'搜尋關鍵字'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        maxPrice: { type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'number'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, description: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'價格上限'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    annotations: { readOnlyHint: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> execute</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> res</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/api/search?'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> URLSearchParams</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(input));</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { content: [{ type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'text'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, text: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> res.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">json</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()) }] };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br></div></div><p>幾個細節值得特別提一下。<code>description</code> 是模型判斷要不要呼叫這個 tool 的主要依據，寫成自然語言、把觸發情境講清楚會比較可靠。
<code>inputSchema</code> 就是 JSON Schema，enum、required、minimum 這些常見屬性都支援。
<code>annotations.readOnlyHint: true</code> 代表這個 tool 不會改動狀態，Agent 可以直接呼叫不用確認。
<code>execute</code> 拿到結構化 input，規格把回傳定成 <code>Promise&lt;any&gt;</code> 沒強制結構；實務上為了跟 MCP 生態（<code>@mcp-b/global</code> polyfill、各家 MCP client）相容，慣例會包成 <code>{ content: [{ type: 'text', text: ... }] }</code> 這種 MCP-style wrapper。整個流程跟 DOM 無關。</p>
<p>會改狀態或有敏感操作的 tool，慣例是透過 <code>client.requestUserInteraction()</code> 把確認權交還給使用者。這個 API 已經寫進規格，但具體演算法（例如瀏覽器要怎麼呈現確認對話框）還是 TODO，所以現階段更像是安全最佳實務、還不算強制規範：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">navigator.modelContext.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">registerTool</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'place_order'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  description: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'送出訂單'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> execute</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">client</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> confirmed</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> client.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">requestUserInteraction</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> showConfirmationDialog</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ items: cart.items, total });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">confirmed) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { content: [{ type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'text'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, text: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ status: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'cancelled'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) }] };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> order</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> placeOrder</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(cart, input);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { content: [{ type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'text'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, text: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(order) }] };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br></div></div><p>規格裡還規劃了一套「declarative API」，讓既有的 <code>&lt;form&gt;</code> 加 <code>toolname</code> / <code>tooldescription</code> / <code>toolautosubmit</code> 三個屬性就能自動轉成 tool。主規格文件裡這一章還沒寫完，不過 working group 開了 <a href="https://github.com/webmachinelearning/webmcp/issues/22" target="_blank" rel="noreferrer">GitHub issue #22</a> 跟一個 <a href="https://github.com/WebMCP-org/WebMCP-declarative-example" target="_blank" rel="noreferrer">declarative-example</a> repo 在推進。動手做的時候還是先走 imperative 這條比較實在。</p>
<p>其他瀏覽器沒原生 API 時可以用 <a href="https://www.npmjs.com/package/@mcp-b/global" target="_blank" rel="noreferrer"><code>@mcp-b/global</code></a> polyfill，一行 script tag 就好，預設「有原生就不動、沒原生就補上一個功能相容的 <code>navigator.modelContext</code>」。要注意的是 polyfill 只補 API 表面，一般瀏覽器<strong>沒有內建 Agent</strong> 會去呼叫它，要真的讓 Agent 自動操作還是得 Chrome Canary；自己串一個 LLM 當驅動則隨便哪個瀏覽器都行，demo repo 的 Gemini 範例就是這樣跑的。</p>
<h2 id="webmcp-帶來什麼效益" tabindex="-1">WebMCP 帶來什麼效益 <a class="header-anchor" href="#webmcp-帶來什麼效益" aria-label="Permalink to “WebMCP 帶來什麼效益”">&#8203;</a></h2>
<p>從三個角色各自看一次。</p>
<p><strong>對網站開發者</strong>最大的誘因是<strong>不用為 Agent 重做一套 API</strong>。原本頁面裡就有的 JavaScript function，包個 <code>registerTool</code> 就能被 Agent 使用，邏輯不用重寫；想限縮 Agent 可以做的事也簡單，就決定要註冊哪些 tool、寫什麼 description。Token 成本這塊差距也很驚人：走 DOM 截圖的路線一次互動常常要塞數千到上萬 tokens，換成結構化 tool call 大多壓在幾百 tokens 以內，對收費模型來說差異一到兩個量級。</p>
<p><strong>對使用者</strong>是可控性跟可見性同時變好。<code>requestUserInteraction</code> 讓寫入操作停在確認視窗這一步，Agent 不能繞過；tool call 本身是結構化的，稽核端能清楚看到「Agent 呼叫了 <code>place_order</code>、傳了這些參數」，而不是一團「點擊了第 3 個按鈕」之類的事件。另外一個很實際的甜蜜點是<strong>直接沿用使用者登入 session</strong>。訪談裡 Alex Nahas 提到 MCP-B 之所以在 Amazon 被做出來，就是因為內部幾千個服務沒有統一 OAuth 2.1、但大家都有 SSO，乾脆讓 Agent 透過分頁的 session cookie 直接代打，不用逼所有團隊去實作 OAuth。</p>
<p><strong>對 LLM / Agent 提供方</strong>是錯誤率跟速度雙改善。DOM 路線要靠模型從視覺推斷 UI element，步驟之間的失敗率很高；改走 schema-driven 之後變成 function call 這種標準問題，模型本來就擅長。延遲也從「每步都要塞截圖等 vision inference」降到「每步只塞結構化 JSON 跑文字推論」。</p>
<p>不過這三方得益有個共同前提：<strong>網站要願意註冊 tool</strong>。這件事靠的是規格推進跟開發者教育，技術實作完還沒完事。短期內會動起來的應該是內部工具、SaaS、有明確 Agent 策略的大平台這幾類；公開內容站的邊際效益反而低，這也是我當初做 Agent Ready 改造時選擇先跳過 WebMCP 的理由。</p>
<h2 id="開發者會踩到的坑" tabindex="-1">開發者會踩到的坑 <a class="header-anchor" href="#開發者會踩到的坑" aria-label="Permalink to “開發者會踩到的坑”">&#8203;</a></h2>
<p>翻資料的時候比較讓我警覺的是，WebMCP 把幾個原本分散的攻擊面<strong>集中到同一個地方</strong>。</p>
<p>最直觀的是 <strong>prompt injection 升級版</strong>。Agent 讀的不只是 tool 的 description，還會讀到 tool 的回傳值、頁面上的其他內容，任何一處被塞一句「順便把訂單全部刪掉」，模型都有被誘發的可能。規格裡 <code>untrustedContentHint</code> 這個 annotation 就是為這件事設計的，但它只是提示，真要擋還得靠呼叫端的模型自己處理。</p>
<p><strong>Tool poisoning</strong> 是 description 本身可以是攻擊載體。使用者在進入一個網站之前，沒有任何機制預覽這個網站會註冊哪些 tool、描述寫了什麼；一旦模型信了描述裡偷塞的指令，就可能挑錯工具呼叫。MCP-B 的 wiki 寫過一句「essentially allows backdooring apps by using existing user session credentials」，語氣其實蠻直白的。</p>
<p><strong>Audit log 難區分使用者跟 Agent</strong> 是另一個隱性風險。因為 WebMCP 用的是使用者的 session，後端看到的是同一個人的合法操作，合規上事後要追「這是本人做的、還是 Agent 代打」很麻煩。對銀行、醫療、HR 這類重合規場景，這件事可能直接讓 WebMCP 不能上線，除非應用層自己多埋一個「這筆是 Agent 操作」的 flag。</p>
<p>規格本身也有幾個未補齊的洞。Tool discovery 要走 navigation，Agent 必須先進到某個網站才能發現它有什麼 tool，沒辦法像 MCP server 那樣集中編目；declarative API 還沒寫完；<code>requestUserInteraction</code> 的 UI 樣式目前完全由網站自己畫，使用者對「現在看到的這個確認框到底是 Agent 觸發、還是普通 confirm」識別全靠體感。Chrome 146 的 Early Preview 還要求頁面得是可見的 browsing context，headless 模式不能跑。</p>
<p>之後真的要動手做 WebMCP 的話，我腦裡大概會守的幾條原則：改狀態的 tool 一律過 <code>requestUserInteraction</code>、<code>readOnlyHint</code> 誠實標、server 端當成完全不可信的 public API 做參數驗證、應用層的 log 要能標出 Agent 代打的操作、每頁 tool 數量別塞爆（超過 50 個模型挑錯機率會升高）、description 寫具體一點（含格式限制例如 <code>YYYY-MM-DD</code>）。還有一條跟規格無關但很重要：<strong>給 Agent 的權限切得比使用者本人小一號</strong>，模型被壞 prompt 誘導時才不會闖太大禍。</p>
<h2 id="未來可能的發展" tabindex="-1">未來可能的發展 <a class="header-anchor" href="#未來可能的發展" aria-label="Permalink to “未來可能的發展”">&#8203;</a></h2>
<p>接下來這段是我基於目前規格缺口跟社群討論的推測，看看參考就好。
但如果真的都動起來了，我認為兩三年後的 Web 生態肯定會跟現在很不一樣。</p>
<p><strong>瀏覽器支援</strong>是最有可能先動起來的一塊。Chrome 146 Canary 的原生實作已經出來；Edge 同屬 Chromium 生態又有 Microsoft 參與規格編輯，後續機率相對高，但目前沒有正式時程。Firefox 跟 Safari 只在 W3C working group 看得到工程師身影，沒有公開承諾，跟不跟、什麼時候跟都是未知。整體來說 Chromium 這系接下來的動態最可預期，其他三家就是看。</p>
<p><strong>Declarative API 成熟</strong>會大幅降低接入門檻。現在走 imperative 要寫一套 JS，對大量簡單表單（聯絡我們、訂閱電子報、站內搜尋）其實殺雞用牛刀；等 <code>&lt;form toolname=&quot;...&quot;&gt;</code> 這種宣告式介面補完，接 Agent 會變成類似加 <code>aria-label</code> 的小動作，電商跟 SaaS 的 onboarding 流程是最可能先動起來的一塊。</p>
<p><strong>跨來源 tool 共享</strong>在 v1 被明確排除（只允許同來源註冊 tool），但已經被列為未來規劃。使用場景很清楚：「找台北評分最高的五家餐廳、一次訂全」這種任務需要 Agent 在多個網站之間協調，全走同一個分頁做不到。怎麼在保護使用者資料的前提下允許 cross-origin invocation 是個懸而未決的設計題，類比起來可能會長成 <code>postMessage</code> 加明確使用者確認的混合體。</p>
<p><strong>Tool discoverability</strong> 是另一個未解的題，可能對生態影響比規格本身還大。目前 Agent 必須先 navigate 到某個網站才知道它有什麼 tool，沒有類似 MCP server catalog 的集中索引。社群有人在推 <code>agenticweb.md</code> 這類 machine-readable 發現規範，邏輯大概是「跟 <code>robots.txt</code> 放一起、內容是這個 domain 提供哪些 tool 的結構化清單」。這個方向如果定下來，「SEO」這個詞大概會有一半含義跟著改寫。</p>
<p><strong>PWA 加背景執行</strong>則是比較遠但蠻有意思的可能性。如果 PWA manifest 可以宣告哪些 tool 是「不用打開 UI 就能執行」的，Agent 在沒有可見分頁的情境下也能呼叫，像「每週五下午幫我檢查購物清單，有特價就加入」這種背景代理任務就做得到了。這算是 WebMCP 從「陪使用者操作當前分頁」擴張到「代理使用者在後台跑任務」的一條可能路徑。</p>
<p>這些方向加起來指的是同一件事：<strong>整個 Web 正在從「給人看的頁面」變成「人跟 Agent 共用的互動介面」</strong>。短期內會動起來的大概還是內部工具、SaaS、重點電商；但如果 Edge / Firefox / Safari 一年內都跟上、跨來源跟 discovery 也有共識，兩年後的 Web 生態會跟現在很不一樣。</p>
<h2 id="想動手玩玩-webmcp" tabindex="-1">想動手玩玩 WebMCP <a class="header-anchor" href="#想動手玩玩-webmcp" aria-label="Permalink to “想動手玩玩 WebMCP”">&#8203;</a></h2>
<p>這次的 Demo 我另外放在 <a href="https://github.com/kurotanshi/kuro-roasters-webmcp" target="_blank" rel="noreferrer">kuro-roasters-webmcp</a> 這個 repo，線上版直接開 <strong><a href="https://kuro.tw/kuro-roasters-webmcp/" target="_blank" rel="noreferrer">https://kuro.tw/kuro-roasters-webmcp/</a></strong> 就能玩，方便大家感受一下 WebMCP 的使用流程場景。</p>
<p>Demo 場景是假想的咖啡豆選購店，註冊五個 WebMCP tool（搜尋、看商品、加入購物車、看購物車、結帳），再接上 Gemini function calling，讓讀者能用自然語言直接操作整個頁面，不需 Chrome Canary 也能試，我覺得挺適合拿來玩玩看、也能當作參考範例。 其中的技術細節（TOOL_DEFS 集中管理、polyfill 行為、agent loop 怎麼串 Gemini）寫在那邊的 <a href="https://github.com/kurotanshi/kuro-roasters-webmcp/blob/main/README.md" target="_blank" rel="noreferrer">README</a>，這篇就不展開多說了。</p>
<h2 id="參考資料" tabindex="-1">參考資料 <a class="header-anchor" href="#參考資料" aria-label="Permalink to “參考資料”">&#8203;</a></h2>
<ul>
<li><a href="https://webmachinelearning.github.io/webmcp/" target="_blank" rel="noreferrer">WebMCP 規格草稿</a></li>
<li><a href="https://github.com/webmachinelearning/webmcp" target="_blank" rel="noreferrer">WebMCP GitHub Repo</a></li>
<li><a href="https://developer.chrome.com/blog/webmcp-epp" target="_blank" rel="noreferrer">Chrome 官方 blog：WebMCP is available for early preview</a></li>
<li><a href="https://developer.chrome.com/blog/webmcp-mcp-usage" target="_blank" rel="noreferrer">Chrome 官方 blog：When to use WebMCP and MCP</a></li>
<li><a href="https://www.scalekit.com/blog/webmcp-the-missing-bridge-between-ai-agents-and-the-web" target="_blank" rel="noreferrer">Scalekit：WebMCP explained</a></li>
<li><a href="https://www.arcade.dev/blog/web-mcp-alex-nahas-interview/" target="_blank" rel="noreferrer">Arcade 訪談 Alex Nahas</a></li>
<li><a href="https://github.com/MiguelsPizza/WebMCP/wiki/Known-Security-Issues-With-WebMCP" target="_blank" rel="noreferrer">MCP-B Wiki：Known Security Issues With WebMCP</a></li>
<li><a href="https://www.npmjs.com/package/@mcp-b/global" target="_blank" rel="noreferrer"><code>@mcp-b/global</code> npm 頁面</a></li>
<li><a href="https://ai.google.dev/gemini-api/docs/function-calling" target="_blank" rel="noreferrer">Gemini function calling 官方文件</a></li>
<li><a href="https://github.com/webmcpnet/awesome-webmcp" target="_blank" rel="noreferrer">awesome-webmcp</a></li>
</ul>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>webmcp</category>
            <category>mcp</category>
            <category>ai</category>
            <category>agents</category>
            <category>browser</category>
            <category>w3c</category>
        </item>
        <item>
            <title><![CDATA[讓靜態部落格對 AI Agent 更友善：Is It Agent Ready? 實測與改造紀錄]]></title>
            <link>https://kurohsu.dev/notes/making-vitepress-blog-agent-ready.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/making-vitepress-blog-agent-ready.html</guid>
            <pubDate>Tue, 21 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="讓靜態部落格對-ai-agent-更友善-is-it-agent-ready-實測與改造紀錄" tabindex="-1">讓靜態部落格對 AI Agent 更友善：Is It Agent Ready? 實測與改造紀錄 <a class="header-anchor" href="#讓靜態部落格對-ai-agent-更友善-is-it-agent-ready-實測與改造紀錄" aria-label="Permalink to “讓靜態部落格對 AI Agent 更友善：Is It Agent Ready? 實測與改造紀錄”">&#8203;</a></h1>
<p>最近 Cloudflare 推出了一個有趣的小工具 <a href="https://isitagentready.com/" target="_blank" rel="noreferrer">Is It Agent Ready?</a>，用來評估你的網站對 AI Agent 的友善程度。</p>
<p>剛好拿自己的部落格去掃了一下，分數 8 分 Not Ready，真慘。
看了一輪檢查項目，覺得有些確實值得做，就順手做了兩輪改造，最後把分數推到 58 分 Level 4 Agent-Integrated，也順便把「哪些該做、哪些不必做」的判斷整理起來。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="讓靜態部落格對-ai-agent-更友善-is-it-agent-ready-實測與改造紀錄" tabindex="-1">讓靜態部落格對 AI Agent 更友善：Is It Agent Ready? 實測與改造紀錄 <a class="header-anchor" href="#讓靜態部落格對-ai-agent-更友善-is-it-agent-ready-實測與改造紀錄" aria-label="Permalink to “讓靜態部落格對 AI Agent 更友善：Is It Agent Ready? 實測與改造紀錄”">&#8203;</a></h1>
<p>最近 Cloudflare 推出了一個有趣的小工具 <a href="https://isitagentready.com/" target="_blank" rel="noreferrer">Is It Agent Ready?</a>，用來評估你的網站對 AI Agent 的友善程度。</p>
<p>剛好拿自己的部落格去掃了一下，分數 8 分 Not Ready，真慘。
看了一輪檢查項目，覺得有些確實值得做，就順手做了兩輪改造，最後把分數推到 58 分 Level 4 Agent-Integrated，也順便把「哪些該做、哪些不必做」的判斷整理起來。</p>
<hr>
<h2 id="什麼是-isitagentready-com" tabindex="-1">什麼是 isitagentready.com？ <a class="header-anchor" href="#什麼是-isitagentready-com" aria-label="Permalink to “什麼是 isitagentready.com？”">&#8203;</a></h2>
<p>這是 Cloudflare 維運的一個靜態檢查工具，給它一個網址，就會從幾個面向評估你的網站準備好「被 AI Agent 使用」了沒。這裡的 Agent 指的不只是傳統爬蟲，還包括會代替使用者瀏覽網站、呼叫 API、做決策的新一代 AI 應用。</p>
<p>評分分四大類（另加一個 Commerce 類別為選配，不計分）：</p>
<ul>
<li><strong>Discoverability 可發現性</strong>：sitemap、robots.txt、Link response headers</li>
<li><strong>Content 內容</strong>：是否支援 Markdown Negotiation，也就是當 Agent 要求 markdown 格式時，能否直接給它乾淨的 markdown 而不是 HTML</li>
<li><strong>Bot Access Control 機器人存取控制</strong>：robots.txt 裡是否明確聲明 AI 爬蟲規則、是否有 Content Signals、是否實作 Web Bot Auth</li>
<li><strong>API, Auth, MCP &amp; Skill Discovery</strong>：API Catalog、OAuth/OIDC、MCP Server Card、Agent Skills index、WebMCP 等</li>
</ul>
<p>每個檢查項目都有明確的 Goal、Issue、How to implement，還附上對應 RFC 和實作參考，整體體驗蠻不錯的。</p>
<hr>
<h2 id="初始分數-8-分" tabindex="-1">初始分數：8 分 <a class="header-anchor" href="#初始分數-8-分" aria-label="Permalink to “初始分數：8 分”">&#8203;</a></h2>
<p>第一次掃描結果很慘：</p>
<p><img src="/images/notes/agent-ready-scan-8.png" alt="第一次掃描結果只有 8 分，顯示 Not Ready"></p>
<ul>
<li>Discoverability：1/3，只有 sitemap 過關</li>
<li>Content：0/1</li>
<li>Bot Access Control：0/2</li>
<li>API, Auth, MCP &amp; Skill Discovery：0/6</li>
</ul>
<p>VitePress 本身有內建 sitemap，這是唯一白撿的分數，其他全軍覆沒，笑死。</p>
<hr>
<h2 id="第一輪改造-robots-txt-與-link-headers" tabindex="-1">第一輪改造：robots.txt 與 Link headers <a class="header-anchor" href="#第一輪改造-robots-txt-與-link-headers" aria-label="Permalink to “第一輪改造：robots.txt 與 Link headers”">&#8203;</a></h2>
<p>這兩項是成本最低的全壘打，做完分數從 8 直接跳到 42（Level 2 Bot-Aware）。</p>
<h3 id="robots-txt" tabindex="-1">robots.txt <a class="header-anchor" href="#robots-txt" aria-label="Permalink to “robots.txt”">&#8203;</a></h3>
<p>在 <code>docs/public/robots.txt</code> 建立一份同時滿足三個檢查項目的 robots.txt：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>User-agent: *</span></span>
<span class="line"><span>Allow: /</span></span>
<span class="line"><span></span></span>
<span class="line"><span># AI crawlers 明確允許（技術筆記希望被 AI 搜尋發現）</span></span>
<span class="line"><span>User-agent: GPTBot</span></span>
<span class="line"><span>Allow: /</span></span>
<span class="line"><span></span></span>
<span class="line"><span>User-agent: ClaudeBot</span></span>
<span class="line"><span>Allow: /</span></span>
<span class="line"><span></span></span>
<span class="line"><span>User-agent: PerplexityBot</span></span>
<span class="line"><span>Allow: /</span></span>
<span class="line"><span></span></span>
<span class="line"><span>User-agent: Google-Extended</span></span>
<span class="line"><span>Allow: /</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Content Signals (https://contentsignals.org)</span></span>
<span class="line"><span># search=yes: 允許搜尋索引</span></span>
<span class="line"><span># ai-input=yes: 允許作為 AI 即時回答的輸入</span></span>
<span class="line"><span># ai-train=no: 不允許作為訓練資料</span></span>
<span class="line"><span>Content-Signal: search=yes, ai-input=yes, ai-train=no</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Sitemap: https://kurohsu.dev/sitemap.xml</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br></div></div><p>這邊有幾個設計值得說明：</p>
<p>第一，對 AI 爬蟲我選擇 <code>Allow</code>。技術筆記本來就是寫給人看的，被 AI 在回答時引用到對流量反而是加分。</p>
<p>第二，<code>Content-Signal</code> 是 Cloudflare 在 2025 年提出、夾在 robots.txt 裡的新指令（詳見 <a href="https://blog.cloudflare.com/content-signals-policy/" target="_blank" rel="noreferrer">Content Signals Policy</a> 與 <a href="https://contentsignals.org/" target="_blank" rel="noreferrer">contentsignals.org</a>）。在它出現之前，robots.txt 只能用 <code>Allow</code> / <code>Disallow</code> 粗略表達「能不能爬」，但「爬了之後能拿去做什麼」一直是模糊地帶。Content Signals 把內容用途拆成三個獨立訊號，每一個都用 <code>yes</code> 或 <code>no</code> 表達：</p>
<ul>
<li><code>search</code>：允許被用在<strong>傳統搜尋索引</strong>，也就是回連結跟短摘要的那種搜尋結果，不包含 AI 生成的摘要</li>
<li><code>ai-input</code>：允許被 LLM 在<strong>即時回答</strong>時當作引用來源，典型情境是 RAG（retrieval-augmented generation）</li>
<li><code>ai-train</code>：允許被拿去<strong>訓練或微調 AI 模型</strong></li>
</ul>
<p>寫法是在 <code>User-agent</code> 區塊裡加一行 <code>Content-Signal: ...</code>，多個訊號用逗號分隔。沒列到的訊號代表「不表達意見」，而不是預設反對。</p>
<p>我的選擇是 <code>search=yes, ai-input=yes, ai-train=no</code>，允許被搜尋與即時引用，但不允許當訓練素材。這裡順帶澄清一個我自己也差點搞錯的細節：Cloudflare 託管 robots.txt 的<strong>預設值其實是 <code>search=yes, ai-train=no</code></strong>，並不會代替使用者自動聲明 <code>ai-input</code>，官方理由是無法預判客戶偏好，不想替人決定。</p>
<p>不過有一點要先講清楚：Content Signals <strong>只是聲明，不是技術強制</strong>。它依賴各家 AI 爬蟲自主遵守，本質上跟 robots.txt 的 <code>Disallow</code> 是同樣道理。這不是防火牆，比較像是把意願攤在陽光下，讓合規的 bot 有所依據，也讓後續的法律或政策討論有個可以指的對象。</p>
<p>第三，<code>Sitemap:</code> 指令放最後，讓爬蟲不用自己猜 sitemap 在哪，雖然傳統但很實用。</p>
<h3 id="link-response-headers" tabindex="-1">Link response headers <a class="header-anchor" href="#link-response-headers" aria-label="Permalink to “Link response headers”">&#8203;</a></h3>
<p><a href="https://www.rfc-editor.org/rfc/rfc8288" target="_blank" rel="noreferrer">RFC 8288</a> 定義的 <code>Link</code> header，目的是讓 Agent 不需要解析 HTML 就能找到站台的重要資源。</p>
<p>這個站架在 Vercel 上，所以在 <code>vercel.json</code> 加：</p>
<div class="language-json line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "headers"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "source"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/(.*)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "headers"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">          "key"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Link"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">          "value"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"&#x3C;/sitemap.xml>; rel=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">sitemap</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">, &#x3C;/feed.xml>; rel=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">alternate</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">; type=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">application/rss+xml</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">, &#x3C;/atom.xml>; rel=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">alternate</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">; type=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">application/atom+xml</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">, &#x3C;/feed.json>; rel=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">alternate</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">; type=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">application/feed+json</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br></div></div><p>一條 Link header 指向 sitemap、RSS、Atom、JSON Feed，這樣 Agent 拿到首頁 response 的當下就能找到所有訂閱管道。</p>
<p>做完這兩項，重新掃描：<strong>42 分</strong>。Discoverability 3/3，Bot Access Control 2/3（Web Bot Auth 是選配的加密簽章驗證，對個人站台來說過度了），短時間內能撿的分都撿起來了。</p>
<p><img src="/images/notes/agent-ready-scan-42.png" alt="第二次掃描結果 42 分，達到 Bot-Aware Level 2"></p>
<hr>
<h2 id="第二輪改造-markdown-negotiation-與-agent-skills" tabindex="-1">第二輪改造：Markdown Negotiation 與 Agent Skills <a class="header-anchor" href="#第二輪改造-markdown-negotiation-與-agent-skills" aria-label="Permalink to “第二輪改造：Markdown Negotiation 與 Agent Skills”">&#8203;</a></h2>
<p>第一輪處理掉了「只需要加檔案就能解」的項目。接下來要拉分，只剩下兩個實際對 Agent 有用的：Markdown Negotiation（Content 類）與 Agent Skills index（API 類）。</p>
<h3 id="markdown-negotiation" tabindex="-1">Markdown Negotiation <a class="header-anchor" href="#markdown-negotiation" aria-label="Permalink to “Markdown Negotiation”">&#8203;</a></h3>
<p>Agent 跟瀏覽器的需求不一樣。瀏覽器要 HTML 加樣式，Agent 只想要乾淨的文本。Markdown Negotiation 的做法是，同一個 URL 依據 request 的 <code>Accept</code> header 回傳不同格式：瀏覽器給 HTML，Agent 要求 <code>text/markdown</code> 就給 markdown。</p>
<p>我的做法分兩步。</p>
<p>第一步，在 VitePress <code>buildEnd</code> hook 加一個產生器，把每篇文章的原始 <code>.md</code>（去掉 frontmatter）輸出到 dist 對應的扁平路徑：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// scripts/generateMarkdownFiles.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> generateMarkdownFiles</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">config</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> SiteConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> outDir</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> config.outDir</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> docsDir</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">resolve</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(config.srcDir)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> category</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> of</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'notes'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'learn'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'misc'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> files</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> walkMarkdownFiles</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(docsDir, category))</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> absPath</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> of</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> files) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> raw</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">readFileSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(absPath, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'utf-8'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">frontmatter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> matter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(raw)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (frontmatter.draft) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">continue</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> slug</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">basename</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(absPath).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">md</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> outPath</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outDir, category, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}.md`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">mkdirSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">dirname</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outPath), { recursive: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">writeFileSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outPath, content.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">trimStart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(), </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'utf-8'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ... 還會產生 index.md 與 about.md</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br></div></div><p>產出後，<code>docs/.vitepress/dist/</code> 底下就會同時有 <code>notes/article.html</code> 和 <code>notes/article.md</code> 兩個檔案。</p>
<p>第二步，在 <code>vercel.json</code> 加條件式 rewrite 和 Content-Type override：</p>
<div class="language-json line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "rewrites"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "source"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/:category(notes|learn|misc)/:slug.html"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "has"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"header"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"key"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"accept"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"value"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">".*text/markdown.*"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ],</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "destination"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/:category/:slug.md"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ],</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "headers"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "source"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/:category(notes|learn|misc)/:slug.html"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "has"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"header"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"key"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"accept"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"value"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">".*text/markdown.*"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ],</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "headers"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"key"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Content-Type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"value"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"text/markdown; charset=utf-8"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"key"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Vary"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">"value"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Accept"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br></div></div><p>重點是 <code>has</code> 條件，只有當 request header 含 <code>Accept: text/markdown</code> 時才會觸發 rewrite，瀏覽器的 Accept 不包含這個值，日常瀏覽完全不受影響。</p>
<p><code>Vary: Accept</code> 則是告訴 CDN 要依照 Accept 分別快取不同版本，避免把 markdown 版本回給瀏覽器，或反過來。</p>
<h3 id="agent-skills-index" tabindex="-1">Agent Skills index <a class="header-anchor" href="#agent-skills-index" aria-label="Permalink to “Agent Skills index”">&#8203;</a></h3>
<p>這是 Cloudflare 推動中的新興 discovery 規範（目前 RFC 還在 v0.2.0，規格仍在演進），位置在 <code>/.well-known/agent-skills/index.json</code>，用來讓站台「告訴 Agent 我這裡能做什麼」。</p>
<p>對純內容站來說，合理的 skill 有兩個：</p>
<ol>
<li><code>read-article-as-markdown</code>：Agent 可以透過 Accept negotiation 拿到任何文章的 markdown</li>
<li><code>discover-articles</code>：Agent 可以透過 sitemap、RSS、tag 頁等方式列出文章</li>
</ol>
<p>我在 <code>docs/public/.well-known/agent-skills/</code> 建了兩個 skill 描述檔（markdown 格式），再寫一個 build script 讀取這些檔案、計算 sha256，輸出 <code>index.json</code>：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// scripts/generateAgentSkills.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> index</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $schema: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://schemas.agentskills.io/discovery/0.2.0/schema.json'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  skills: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">SKILLS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">skill</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    name: skill.name,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'skill-md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    description: skill.description,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    url: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">skill</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">path</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    digest: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sha256Digest</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outDir, skill.path))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><p>Skill 描述檔用 markdown 寫，方便人類也能讀；digest 則提供完整性比對的基礎，讓消費端<strong>有能力</strong>驗證檔案沒被動過手腳。不過這條信任鏈成不成立，終究還是要看 Agent 那端是否真的實作了驗證流程。</p>
<hr>
<h2 id="為什麼剩下的五項我選擇不做" tabindex="-1">為什麼剩下的五項我選擇不做 <a class="header-anchor" href="#為什麼剩下的五項我選擇不做" aria-label="Permalink to “為什麼剩下的五項我選擇不做”">&#8203;</a></h2>
<p>檢查報告還剩五個紅燈：API Catalog、OAuth/OIDC Discovery、OAuth Protected Resource、MCP Server Card、WebMCP。
這些我選擇暫時不實作，<strong>硬湊只會變成 cargo-cult</strong>，分數是好看了，但對個人部落格來說毫無實際意義，甚至可能因為多了沒必要的 endpoint 反而誤導 Agent。</p>
<p><strong>API Catalog (<a href="https://www.rfc-editor.org/rfc/rfc9727" target="_blank" rel="noreferrer">RFC 9727</a>)</strong> 要求列出站上的 API 加 OpenAPI spec 加 health endpoint。純靜態部落格根本沒有 API，把 <code>feed.json</code> 當 API 硬塞是在誤導 Agent。</p>
<p><strong>OAuth/OIDC Discovery</strong> 需要你有 OAuth server 或 OpenID provider。這個站零認證，發布空的 metadata 會告訴 Agent「這裡可以拿 token」然後什麼都沒有，反而破壞信任。</p>
<p><strong>OAuth Protected Resource</strong> 同上，要有「被保護的資源」才有意義。</p>
<p><strong>MCP Server Card</strong> 是 MCP server 的名片，前提是你真的跑了一個 MCP server。只貼 card 沒有對應 endpoint，Agent 連過來就 404。這倒是一個有點意思的延伸題目，像是做一個「用 MCP 協議查部落格內容」的 server，不過那是另一個獨立專案了，不是加個檔案能解決的事。</p>
<p><strong>WebMCP</strong> 是目前唯一對內容站可能有意義的，但現階段還在 Chrome 早期預覽計畫（Early Preview Program）階段，要報名才能拿到文件和 demo，spec 也還在變動。靜態網站為了這件事特別載入 client runtime script 成本不對等，等跨瀏覽器都支援再來做 CP 值會好很多。（後來還是花時間補做了一輪，研究筆記跟 demo 寫在 <a href="/learn/learning-webmcp.html">初試 WebMCP</a> 那篇。）</p>
<hr>
<h2 id="最終分數與一些觀察" tabindex="-1">最終分數與一些觀察 <a class="header-anchor" href="#最終分數與一些觀察" aria-label="Permalink to “最終分數與一些觀察”">&#8203;</a></h2>
<p>兩輪改造結束，分數從 8 推到 <strong>58 分 Level 4 Agent-Integrated</strong>：</p>
<p><img src="/images/notes/agent-ready-scan-58.png" alt="最終掃描結果 58 分 Level 4 Agent-Integrated"></p>
<p>Discoverability、Content、Bot Access Control 三個類別都是滿分，API / Auth / MCP &amp; Skill Discovery 拿到 1/6（Agent Skills index 過關）。對一個純內容靜態部落格來說已經接近上限了，剩下的 40 分是給有 API、有認證、有 MCP 基礎設施的站台準備的。</p>
<p>有幾個觀察值得記錄：</p>
<p>第一，<strong>「分數」不是目的</strong>。isitagentready.com 的檢查清單本質上是一份 best-practices，不同類型的站台適用的項目也不一樣。個人部落格硬追到 100 分，代表你在發布一堆假的 endpoint，對真正的 Agent 反而是雜訊。</p>
<p>第二，<strong>Markdown Negotiation 是最實用的一項</strong>。當 AI Agent 幫使用者讀你的文章時，省掉 HTML 解析這一層可以大幅降低雜訊，這個改動對內容可讀性最有感，分數反而是順帶的。</p>
<p>第三，Cloudflare 這套檢查會順帶把 robots.txt 的 AI 爬蟲規則、Content Signals 這些新規範一次推給你。就算不打算做 MCP，前半段的 Discoverability 跟 Bot Access Control 還是很值得跑一次，花不到十分鐘就能補完。</p>
<p>第四點是個保留：這篇目前都停在「<strong>能力層</strong>」，也就是「站台現在有能力被乾淨地抓、被 well-known 路徑發現」。但實際上到底有沒有 Agent 真的送 <code>Accept: text/markdown</code>、<code>/.well-known/agent-skills/index.json</code> 被誰抓走過、Link header 有沒有被解析，這些「<strong>成果層</strong>」的問題都還得靠 access log 才能回答，而我目前還沒有足夠資料。等累積一段時間的觀測再來寫一篇 follow-up 對照，應該會比分數本身有意義得多。</p>
<blockquote>
<p>一個月後的 follow-up 寫在 <a href="/notes/agent-ready-one-month-followup.html">Is It Agent Ready? 一個月後的 bot 流量實測</a>：28 天 4,264 筆 bot 流量資料攤開，當初三條觀察有兩條成立、一條寫錯了。</p>
</blockquote>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vitepress</category>
            <category>ai</category>
            <category>agents</category>
            <category>vercel</category>
            <category>seo</category>
        </item>
        <item>
            <title><![CDATA[Claude Code Skills 與 Skill Creator：打造你的專屬 AI 工具包]]></title>
            <link>https://kurohsu.dev/notes/claude-code-skills-and-skill-creator.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/claude-code-skills-and-skill-creator.html</guid>
            <pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="claude-code-skills-與-skill-creator-打造你的專屬-ai-工具包" tabindex="-1">Claude Code Skills 與 Skill Creator：打造你的專屬 AI 工具包 <a class="header-anchor" href="#claude-code-skills-與-skill-creator-打造你的專屬-ai-工具包" aria-label="Permalink to “Claude Code Skills 與 Skill Creator：打造你的專屬 AI 工具包”">&#8203;</a></h1>
<p>用 Claude Code 一段時間之後，漸漸發現有些任務每次都要重新跟 Claude 解釋一遍背景：「我們的 commit 格式是這樣」、「記得用這個框架的慣例」、「這個專案有這些規則」。</p>
<p>Skills 就是為了解決這個問題而生的，讓你把這些重複的知識和流程打包起來，之後直接叫出來用就好。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="claude-code-skills-與-skill-creator-打造你的專屬-ai-工具包" tabindex="-1">Claude Code Skills 與 Skill Creator：打造你的專屬 AI 工具包 <a class="header-anchor" href="#claude-code-skills-與-skill-creator-打造你的專屬-ai-工具包" aria-label="Permalink to “Claude Code Skills 與 Skill Creator：打造你的專屬 AI 工具包”">&#8203;</a></h1>
<p>用 Claude Code 一段時間之後，漸漸發現有些任務每次都要重新跟 Claude 解釋一遍背景：「我們的 commit 格式是這樣」、「記得用這個框架的慣例」、「這個專案有這些規則」。</p>
<p>Skills 就是為了解決這個問題而生的，讓你把這些重複的知識和流程打包起來，之後直接叫出來用就好。</p>
<hr>
<h2 id="什麼是-skills" tabindex="-1">什麼是 Skills？ <a class="header-anchor" href="#什麼是-skills" aria-label="Permalink to “什麼是 Skills？”">&#8203;</a></h2>
<p>Skills 是一種「可攜式的指令包」，本質上是一個資料夾，裡面放了 Claude 需要知道的知識、腳本和資源。
當你啟動一個 Skill，Claude 就會自動載入對應的指令，不需要你每次重新說明。</p>
<p>官方的 <a href="https://github.com/anthropics/skills" target="_blank" rel="noreferrer">anthropics/skills</a> 倉庫提供了幾個現成的範例：</p>
<ul>
<li><strong>pdf / docx / xlsx / pptx</strong>：讀取和分析各種文件格式</li>
<li><strong>webapp-testing</strong>：用 Playwright 測試本地網頁應用程式</li>
<li><strong>algorithmic-art</strong>：用 p5.js 生成演算法藝術</li>
<li><strong>brand-guidelines</strong>：套用品牌設計規範</li>
<li><strong>mcp-builder</strong>：快速建立 MCP 工具</li>
</ul>
<p>這些只是範本，更有趣的是你可以自己做一個符合自己需求的 Skill。</p>
<hr>
<h2 id="skills-的結構" tabindex="-1">Skills 的結構 <a class="header-anchor" href="#skills-的結構" aria-label="Permalink to “Skills 的結構”">&#8203;</a></h2>
<p>每個 Skill 是一個資料夾，裡面至少要有一個 <code>SKILL.md</code>：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>my-skill/</span></span>
<span class="line"><span>├── SKILL.md          # 必要，Skill 的核心定義</span></span>
<span class="line"><span>├── scripts/          # 可選，Python / Bash / JS 腳本（確定性任務）</span></span>
<span class="line"><span>├── references/       # 可選，補充文件，供 Claude 按需參考</span></span>
<span class="line"><span>└── assets/           # 可選，輸出用的靜態資源（模板、字體、圖片等）</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p><code>scripts/</code>、<code>references/</code>、<code>assets/</code> 三個目錄的定位不一樣，值得區分清楚：</p>
<ul>
<li><code>scripts/</code> 放的是 Claude 會去執行的程式碼，像是確定性的格式轉換、資料處理</li>
<li><code>references/</code> 放的是文件，Claude 在需要時會讀進 context 參考</li>
<li><code>assets/</code> 放的是輸出用的檔案，像是 PowerPoint 模板、字體、Logo，不是讓 Claude 讀的，而是讓 Claude 拿來用的</li>
</ul>
<hr>
<h2 id="skill-md-的格式" tabindex="-1">SKILL.md 的格式 <a class="header-anchor" href="#skill-md-的格式" aria-label="Permalink to “SKILL.md 的格式”">&#8203;</a></h2>
<p><code>SKILL.md</code> 用 YAML frontmatter 加上 Markdown 內文：</p>
<div class="language-markdown line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">my-skill</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">description</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">This skill should be used when the user asks to "do X", "handle Y", or mentions Z-related topic. Describe what this skill does and when to trigger it.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># 詳細的操作指令</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">...</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>Frontmatter 有幾個欄位：</p>
<table tabindex="0">
<thead>
<tr>
<th>欄位</th>
<th>必填</th>
<th>說明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td>✓</td>
<td>小寫字母、數字、連字號，最長 64 字，需與資料夾同名</td>
</tr>
<tr>
<td><code>description</code></td>
<td>✓</td>
<td>說明 Skill 做什麼、何時觸發，最長 1024 字</td>
</tr>
<tr>
<td><code>license</code></td>
<td>—</td>
<td>授權聲明</td>
</tr>
<tr>
<td><code>compatibility</code></td>
<td>—</td>
<td>環境需求（套件、網路等）</td>
</tr>
<tr>
<td><code>allowed-tools</code></td>
<td>—</td>
<td>預先核准的工具清單（實驗性）</td>
</tr>
</tbody>
</table>
<p>有兩個寫法慣例值得注意。Description 要用<strong>第三人稱</strong>格式，也就是「This skill should be used when the user asks to...」，不是「Use this skill when...」。</p>
<p>至於 SKILL.md 的內文，則建議用<strong>祈使句</strong>，動詞開頭的直接指令，而不是「You should do...」這樣的第二人稱。</p>
<hr>
<h2 id="三層載入機制" tabindex="-1">三層載入機制 <a class="header-anchor" href="#三層載入機制" aria-label="Permalink to “三層載入機制”">&#8203;</a></h2>
<p>Skills 設計了一個漸進式載入機制，避免每次都把所有資訊塞進 context：</p>
<p><strong>第一層：隨時在場（約 100 words）</strong>
只有 <code>name</code> 和 <code>description</code> 會一直在 context 裡。Claude 用這個判斷哪個 Skill 需要啟動。</p>
<p><strong>第二層：啟動時載入</strong>
當 Claude 判斷你的需求符合這個 Skill，才會把 <code>SKILL.md</code> 的內文讀進來。
建議控制在 500 行以內，大約 1,500 到 2,000 words 是理想範圍。</p>
<p><strong>第三層：按需載入</strong>
<code>scripts/</code>、<code>references/</code>、<code>assets/</code> 只在真的需要時才載入。
大量的參考文件或資料放這裡，不會一開始就佔用 context。</p>
<p>這個機制讓你可以安裝很多個 Skill，但 token 消耗依然受控。</p>
<hr>
<h2 id="一個真實的-skill-範例" tabindex="-1">一個真實的 Skill 範例 <a class="header-anchor" href="#一個真實的-skill-範例" aria-label="Permalink to “一個真實的 Skill 範例”">&#8203;</a></h2>
<p>xlsx Skill 的 description 是個很好的範本，看它怎麼把觸發情境列得非常具體：</p>
<div class="language-yaml line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">yaml</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">---</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">xlsx</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">description</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Use this skill any time a spreadsheet file is the primary input or output.</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  This means any task where the user wants to: open, read, edit, or fix an existing</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  charting, cleaning messy data); create a new spreadsheet from scratch or from other</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  data sources; or convert between tabular file formats. Trigger especially when the</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  user references a spreadsheet file by name or path — even casually (like </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">the xlsx</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  in my downloads</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">) — and wants something done to it or produced from it. Do NOT trigger</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  when the primary deliverable is a Word document, HTML report, standalone Python script,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  database pipeline, or Google Sheets API integration, even if tabular data is involved."</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">license</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">Proprietary. LICENSE.txt has complete terms</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">---</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br></div></div><p>注意幾點：觸發條件列得很細，包含使用者可能說的各種方式；更重要的是，它還明確說了<strong>什麼情況下不要觸發</strong>，避免搶到不屬於自己的任務。</p>
<p>SKILL.md 的內文也同樣具體，以 xlsx Skill 為例，裡面直接給了操作模式、錯誤處理流程，甚至連 Excel 公式的顏色編碼規範都定義好了，Claude 執行任務時完全不需要自己摸索。</p>
<hr>
<h2 id="安裝現有的-skills" tabindex="-1">安裝現有的 Skills <a class="header-anchor" href="#安裝現有的-skills" aria-label="Permalink to “安裝現有的 Skills”">&#8203;</a></h2>
<p>官方 Skills 可以透過 plugin 市集安裝：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">/plugin</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> marketplace</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> anthropics/skills</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>裝好之後，直接自然地跟 Claude 說就可以觸發，不用記指令：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>幫我分析這份 contract.pdf 裡的付款條款</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>把 sales_report.xlsx 的季度資料加上一欄 profit margin</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>Claude 會自動判斷要用哪個 Skill 來處理。</p>
<hr>
<h2 id="用-skill-creator-打造自己的-skill" tabindex="-1">用 Skill Creator 打造自己的 Skill <a class="header-anchor" href="#用-skill-creator-打造自己的-skill" aria-label="Permalink to “用 Skill Creator 打造自己的 Skill”">&#8203;</a></h2>
<p>官方提供了 <a href="https://claude.com/plugins/skill-creator" target="_blank" rel="noreferrer">Skill Creator</a> plugin，這是專門用來開發和測試 Skill 的工具包。它本身也是一個 Skill，安裝後只要告訴 Claude 你想建立或改善一個 Skill，它就會自動觸發。</p>
<p>整個開發流程是一個迭代迴圈：定義需求、撰寫草稿、跑測試、看結果、修改、再跑。</p>
<p>具體來說：</p>
<p>首先，Skill Creator 會問你幾個問題：這個 Skill 要做什麼、什麼情況下應該觸發、輸出格式是什麼、有沒有邊界情況要注意。問完之後，它會幫你產生 <code>SKILL.md</code> 草稿，並建立 2–3 個測試案例存成 <code>evals/evals.json</code>：</p>
<div class="language-json line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "skill_name"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"my-skill"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "evals"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "id"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "prompt"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"使用者的實際需求"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "expected_output"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"期望的輸出描述"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "files"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: []</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><h3 id="測試怎麼跑" tabindex="-1">測試怎麼跑 <a class="header-anchor" href="#測試怎麼跑" aria-label="Permalink to “測試怎麼跑”">&#8203;</a></h3>
<p>接著 Skill Creator 會對每個測試案例<strong>同時</strong>跑兩個版本：一個「有 Skill」，一個「沒有 Skill」（改善現有 Skill 時則是跑舊版本作為 baseline）。
兩個版本平行執行，跑完之後透過 eval viewer 讓你瀏覽輸出結果，留下反饋。</p>
<p>在等待執行的過程中，Skill Creator 不會閒著，它會同時草擬定量評估的 assertions，也就是針對每個測試案例寫下可客觀判斷的期望，例如「輸出包含欄位 X」、「使用了腳本 Y」。
等結果出來，就會有 Grader 子代理自動評估每個 assertion 是否通過。</p>
<h3 id="三個幕後子代理" tabindex="-1">三個幕後子代理 <a class="header-anchor" href="#三個幕後子代理" aria-label="Permalink to “三個幕後子代理”">&#8203;</a></h3>
<p>Skill Creator 內建三個專屬子代理，各司其職：</p>
<p><strong>Grader</strong>（評分者）負責把 assertion 對照執行紀錄和輸出檔案逐條評判，通過或不通過、附上具體證據。它有個有趣的工作：除了評估你預先定義的 assertions，它還會主動從輸出中提取隱含的宣稱（例如「此 Skill 產生了 12 個欄位」），然後一一查核，補抓你沒寫到的盲點。更重要的是，它也會批判 evals 本身，如果某個 assertion 太容易通過（例如只檢查檔案存在而不檢查內容），它會指出來。</p>
<p><strong>Comparator</strong>（比較者）是盲測。它收到兩份輸出，但不知道哪份是哪個 Skill 產生的，純粹從「內容正確性、完整性、準確性」和「結構組織、格式、易用性」六個維度各給 1–5 分，加總得出 1–10 的總分，選出贏家。
去掉身份資訊是為了避免偏見。</p>
<p><strong>Analyzer</strong>（分析者）是 Comparator 之後的後處理。它「解盲」，知道了哪份輸出是哪個 Skill 的，然後去讀兩個 Skill 的 SKILL.md 和執行紀錄，找出「贏家為什麼贏、輸家哪裡輸了」，最後給出有優先級（high / medium / low）的改善建議，分類包括指令措辭、腳本工具、範例覆蓋、錯誤處理等。</p>
<h3 id="description-優化" tabindex="-1">Description 優化 <a class="header-anchor" href="#description-優化" aria-label="Permalink to “Description 優化”">&#8203;</a></h3>
<p>Skill 做得差不多之後，Skill Creator 還提供一個獨立的 description 優化流程，專門解決 undertrigger（觸發率不足）的問題。</p>
<p>它會先產生 20 個觸發評估查詢，其中 8–10 個「應該觸發」，8–10 個「不應該觸發」。這些查詢要夠具體，不能是「格式化這份資料」這種空泛描述，而是要像真實使用者會說的：帶有檔案路徑、公司名稱、個人背景的完整情境句。</p>
<p>你看過這些查詢、確認沒問題之後，優化腳本會跑起來：把查詢集 60% 用於訓練、40% 留作測試，每個查詢跑三次取觸發率均值，然後讓 Claude 開啟延伸思考（extended thinking）提出改進的 description，再重新評估，最多迭代五輪。最後用測試集的分數（而非訓練集）選出最佳版本，避免過擬合。</p>
<p>這個流程跑完，你會得到一個更新的 description，Skill Creator 會把前後版本和分數一起展示給你確認。</p>
<h3 id="觸發機制的一個微妙點" tabindex="-1">觸發機制的一個微妙點 <a class="header-anchor" href="#觸發機制的一個微妙點" aria-label="Permalink to “觸發機制的一個微妙點”">&#8203;</a></h3>
<p>有一件事值得知道：Claude 只會在「自己處理不來」的任務上去查 Skill。太簡單的一步請求，例如「讀一下這個 PDF」，即使 description 完全吻合，也可能不觸發 Skill，因為 Claude 覺得自己直接處理就好了。
複雜、多步驟、或需要專業知識的任務才會可靠地觸發。所以測試案例要設計得夠有深度，才能真正測試出 description 的觸發品質。</p>
<hr>
<h2 id="寫好-description-是關鍵" tabindex="-1">寫好 Description 是關鍵 <a class="header-anchor" href="#寫好-description-是關鍵" aria-label="Permalink to “寫好 Description 是關鍵”">&#8203;</a></h2>
<p>測試幾個 Skill 下來，發現 description 寫得好不好，直接決定了 Skill 能不能在對的時機自動觸發。</p>
<p>Skill Creator 的文件裡提到一個觀察：Skill 的問題通常不是「亂觸發」，而是「沒有觸發」（undertrigger）。解法是讓 description 更「主動」，把所有相關的使用情境都明確列出來，即使使用者沒有用精確的關鍵字。</p>
<p>來看個例子，假設你在做一個 dashboard 生成 Skill：</p>
<p>❌ 太模糊，容易漏觸發：</p>
<div class="language-yaml line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">yaml</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">description</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">建立資料視覺化介面。</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>✅ 明確列出觸發情境：</p>
<div class="language-yaml line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">yaml</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">description</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">This skill should be used when the user asks to build a dashboard or data visualization interface. Use this skill whenever the user mentions dashboards, internal metrics, data display, or wants to visualize any kind of company data, even if they don't explicitly say "dashboard".</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>另外，description 也值得說明<strong>不觸發的邊界</strong>，特別是當你的 Skill 和其他 Skill 的領域有重疊時，像 xlsx Skill 那樣明確排除「主要產出是 HTML 報表」的情況。</p>
<hr>
<h2 id="實作時踩過的坑" tabindex="-1">實作時踩過的坑 <a class="header-anchor" href="#實作時踩過的坑" aria-label="Permalink to “實作時踩過的坑”">&#8203;</a></h2>
<h3 id="_1-name-和資料夾名稱要完全一致" tabindex="-1">1. name 和資料夾名稱要完全一致 <a class="header-anchor" href="#_1-name-和資料夾名稱要完全一致" aria-label="Permalink to “1. name 和資料夾名稱要完全一致”">&#8203;</a></h3>
<p><code>SKILL.md</code> 裡的 <code>name</code> 欄位必須和資料夾名稱相同，否則 Skill 無法正確載入。</p>
<h3 id="_2-skill-md-要主動指向子目錄資源" tabindex="-1">2. SKILL.md 要主動指向子目錄資源 <a class="header-anchor" href="#_2-skill-md-要主動指向子目錄資源" aria-label="Permalink to “2. SKILL.md 要主動指向子目錄資源”">&#8203;</a></h3>
<p>Claude 不會自動掃描 <code>references/</code> 裡面有什麼，你需要在 <code>SKILL.md</code> 內文裡明確告訴它：</p>
<div class="language-markdown line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## 延伸資源</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">需要更詳細的規則時，參考：</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold"> **</span><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">`references/formatting-guide.md`</span><span style="--shiki-light:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold">**</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> — 格式規範細節</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold"> **</span><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">`scripts/validate.py`</span><span style="--shiki-light:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold">**</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> — 格式驗證腳本</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>忘記這步的話，那些精心準備的參考文件就白費了。</p>
<h3 id="_3-skill-md-內文不要塞太多" tabindex="-1">3. SKILL.md 內文不要塞太多 <a class="header-anchor" href="#_3-skill-md-內文不要塞太多" aria-label="Permalink to “3. SKILL.md 內文不要塞太多”">&#8203;</a></h3>
<p>SKILL.md 啟動時整個讀進 context，建議控制在 1,500 到 2,000 words 左右。
大量的參考資料、邊界情況說明、詳細範例，都搬到 <code>references/</code> 目錄，按需載入。</p>
<h3 id="_4-description-不要超過-1024-字元" tabindex="-1">4. description 不要超過 1024 字元 <a class="header-anchor" href="#_4-description-不要超過-1024-字元" aria-label="Permalink to “4. description 不要超過 1024 字元”">&#8203;</a></h3>
<p>description 雖然要寫得完整，但有字元上限，而且它一直佔用 context，重點放在「觸發情境」和「做什麼」就好，不需要把整個操作流程都塞進去。</p>
<hr>
<h2 id="自己從零寫一個-skill" tabindex="-1">自己從零寫一個 Skill <a class="header-anchor" href="#自己從零寫一個-skill" aria-label="Permalink to “自己從零寫一個 Skill”">&#8203;</a></h2>
<p>如果不用 Skill Creator，從零開始寫一個最簡單的 Skill 也不複雜，只要一個資料夾加一個 <code>SKILL.md</code>。這裡用一個 commit message 規範的 Skill 來示範整個流程。</p>
<p>先建立資料夾，名稱就是 Skill 的 <code>name</code>：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">mkdir</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -p</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ~/.claude/skills/commit-helper</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>放在 <code>~/.claude/skills/</code> 下的 Skill 是個人全域的，在你的任何專案都有效。
如果只想在特定專案使用，可以放在專案根目錄的 <code>.claude/skills/</code> 下，這個資料夾也可以 commit 進 repo 跟著版控。</p>
<p>不過有個反直覺的地方：當兩個層級存在同名 Skill 時，<strong>全域 Skill 的優先級高於專案 Skill</strong>。所以如果全域已經有一個 <code>commit-helper</code>，專案裡的同名版本不會生效。</p>
<p>接著建立 <code>SKILL.md</code>：</p>
<div class="language-markdown line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">commit-helper</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">description</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">This skill should be used when the user wants to write a commit message,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  asks "how should I commit this", or is about to run git commit. Guides writing</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  conventional commit messages following the Conventional Commits specification.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># Commit Message 指引</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Follow the Conventional Commits format: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">`type(scope): description`</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Types: feat, fix, docs, style, refactor, test, chore</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Rules:</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Keep the subject line under 72 characters</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Use present tense ("add feature", not "added feature")</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> No period at the end of the subject line</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Reference issue numbers when relevant: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">`fix(auth): correct token expiry (#123)`</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Examples:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Input: 新增使用者登入功能，用 JWT 處理 token</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Output: feat(auth): add JWT-based user authentication</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Input: 修復購物車數量計算錯誤</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Output: fix(cart): correct item quantity calculation</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br></div></div><p>這樣就完成了。來拆解每個部分：</p>
<p><strong><code>name</code></strong> 欄位對應資料夾名稱，必須完全一致。命名規則是小寫字母、數字和連字號，不能有大寫。</p>
<p><strong><code>description</code></strong> 是 Claude 判斷要不要啟動這個 Skill 的唯一依據。它一直在 context 裡，所以要寫得精準但不囉嗦。這裡用第三人稱格式（<code>This skill should be used when...</code>），並且明確列出幾種觸發場景：「想寫 commit message」、「問 how should I commit this」、「要跑 git commit」。
這三種說法分別對應不同使用者的口頭習慣，都能觸發。</p>
<p><strong>內文</strong> 用祈使句寫直接指令（<code>Follow</code>、<code>Keep</code>、<code>Use</code>），不用「你應該...」這樣的第二人稱。最後的 Examples 區塊很重要，因為 Input/Output 的對比比起文字描述更直接，Claude 更容易理解期望的輸出格式。</p>
<p>存好之後，開一個新的對話，之後只要說「幫我寫這次的 commit」，Claude 就會套用這套規則來建議 commit message，不用每次再解釋格式。</p>
<p>這個例子雖然簡單，但可以感受到把知識寫成 Skill 帶來的幾個具體好處。</p>
<p>第一個是<strong>一致性</strong>。以前每次請 Claude 寫 commit message，它的風格多少會有差異，有時用過去式、有時用中文。寫成 Skill 之後，每次都會照固定格式輸出，整個 git log 風格統一，之後 changelog 也好自動生成。</p>
<p>第二個是<strong>可以隨時疊加細節</strong>。需求可以一直增長：這個月 code review 之後發現 scope 的寫法有爭議，直接去改 <code>SKILL.md</code> 就好；下次加入新成員，把 Skill 資料夾複製給他，規範同步就完成了。
相比在 chat 裡每次貼上規則，Skill 是可以持續維護的。</p>
<p>第三個是<strong>不佔任何 context</strong>，除非真的要寫 commit。skills 的 description 雖然一直在，但它很短，只要不觸發，完整的 commit 規則不會干擾其他任何對話。
這和在 CLAUDE.md 裡寫死規則不一樣，CLAUDE.md 的內容從打開專案的那一刻起就全部進 context，不管當下的任務是不是跟 commit 有關。</p>
<hr>
<h2 id="skills-和-claude-md-的差異" tabindex="-1">Skills 和 CLAUDE.md 的差異 <a class="header-anchor" href="#skills-和-claude-md-的差異" aria-label="Permalink to “Skills 和 CLAUDE.md 的差異”">&#8203;</a></h2>
<p>用過 CLAUDE.md 的人可能會有個疑問：兩者都是給 Claude 的指令，到底有什麼不同？</p>
<p>簡單說：<strong>CLAUDE.md 是「這個專案的說明書」，Skills 是「你個人的工具包」</strong>。</p>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th>CLAUDE.md</th>
<th>Skills</th>
</tr>
</thead>
<tbody>
<tr>
<td>作用範圍</td>
<td>綁定在某個專案目錄</td>
<td>可攜，跨專案使用</td>
</tr>
<tr>
<td>觸發方式</td>
<td>進入專案時自動載入</td>
<td>Claude 依需求判斷是否啟動</td>
</tr>
<tr>
<td>適合放什麼</td>
<td>這個 repo 特有的規範、架構說明</td>
<td>可重複使用的流程、領域知識</td>
</tr>
<tr>
<td>維護方式</td>
<td>跟著 repo 版控</td>
<td>獨立管理，可分享給他人</td>
</tr>
</tbody>
</table>
<p>舉個具體例子。這個部落格的 VitePress 目錄結構、frontmatter 格式、URL rewrite 邏輯，適合放在 CLAUDE.md，因為這些是這個 repo 特有的，換個專案就沒用了。但「寫 conventional commit」、「處理 Excel 檔案」、「套用品牌設計規範」這類知識，放在 Skills 更合適，因為在任何專案都能直接用。</p>
<p>兩者可以並存，互補。</p>
<p>CLAUDE.md 告訴 Claude「這個專案是什麼」，Skills 告訴 Claude「遇到 X 情況要怎麼做」。</p>
<hr>
<h2 id="團隊開發時怎麼共享-skills" tabindex="-1">團隊開發時怎麼共享 Skills <a class="header-anchor" href="#團隊開發時怎麼共享-skills" aria-label="Permalink to “團隊開發時怎麼共享 Skills”">&#8203;</a></h2>
<p>個人 Skill 寫好之後，下一個問題通常是：怎麼讓整個團隊都用到？</p>
<p>最直接的方式是把 Skills 放進專案的版本控制。在 repo 根目錄建立 <code>.claude/skills/</code>，把 Skill 資料夾放進去，commit 上去，之後 clone 這個 repo 的人就自動拿到這些 Skills。這特別適合和專案強相關的 Skill，例如「按照這個 codebase 的命名慣例產生 component」，放全域沒有意義，但放 repo 裡就能讓所有貢獻者共享同一套規則。</p>
<p>如果 Skill 不屬於某個特定 repo，而是整個組織都想用的通用工具，可以另外建一個獨立的 skills repo，然後用 plugin marketplace 的方式發佈：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">/plugin</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> marketplace</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> your-org/skills</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>這樣成員只要執行一次安裝，就能用到所有共享的 Skills，之後更新也只要在 repo 上改，不用每個人手動同步。anthropics/skills 就是這個模式。</p>
<p>第三個管道是把 Skill 打包成 <code>.skill</code> 檔案。Skill Creator 裡有一個 <code>package_skill.py</code> 腳本，可以把整個 Skill 資料夾打包成單一檔案，方便透過 Slack、email 或 PR 傳給同事，接收方直接安裝就好，不需要接觸 repo。</p>
<hr>
<h2 id="skill-什麼時候該拆、什麼時候該合" tabindex="-1">Skill 什麼時候該拆、什麼時候該合 <a class="header-anchor" href="#skill-什麼時候該拆、什麼時候該合" aria-label="Permalink to “Skill 什麼時候該拆、什麼時候該合”">&#8203;</a></h2>
<p>寫幾個 Skill 之後，難免會碰到這個問題：兩件事該放在同一個 Skill 裡，還是分開？</p>
<p><strong>傾向分開的情況：</strong> 兩個功能的觸發時機根本不同，強行放在一起只會讓 description 越來越難寫，而且 Claude 可能因為無法精確判斷而誤觸發。例如「產生測試案例」和「跑 E2E 測試」，雖然都跟測試有關，但前者是在寫 code 的當下觸發，後者是準備部署的時候觸發，分開讓兩個 description 都能寫得更精準。另外，當 SKILL.md 的內文開始逼近 500 行，也是一個該考慮拆分的訊號，可以把其中一個領域移到獨立 Skill，再用 <code>references/</code> 的方式讓它們按需互相參照。</p>
<p><strong>傾向合在一起的情況：</strong> 兩個任務幾乎總是同時出現，每次用一個必然也用另一個，拆開反而增加維護成本。或者兩個任務共用大量的背景知識，合在一起可以讓這些知識只寫一次。</p>
<p>最實用的判斷方式是問：「這兩件事的觸發場景，我能用一句話說清楚嗎？」如果 description 需要塞很多「也可以用來做 Y，以及 Z」，通常就是一個應該拆分的訊號。</p>
<hr>
<h2 id="總結" tabindex="-1">總結 <a class="header-anchor" href="#總結" aria-label="Permalink to “總結”">&#8203;</a></h2>
<p>Skills 解決的是「重複解釋背景」的問題，把工作流程、規範或領域知識打包成可以隨時叫用的工具。Skill Creator 讓開發流程有了明確的迭代結構；project-level 的 <code>.claude/skills/</code> 讓 Skills 可以跟著 repo 走、隨團隊共享；拆或合的判斷則回歸到一個問題：這個 Skill 的觸發時機，能不能用一句話說清楚。</p>
<p>如果你有某個任務需要每隔幾天就跟 Claude 重新解釋一遍，那大概就是值得做成 Skill 的候選。最小可行的 Skill 只需要一個資料夾加一個 <code>SKILL.md</code>，門檻很低，可以先試試看效果。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>claude</category>
            <category>ai</category>
            <category>claude-code</category>
            <category>productivity</category>
        </item>
        <item>
            <title><![CDATA[WebConf 2025 延伸閱讀：Vue.js 與 AI 協作的開發新思維]]></title>
            <link>https://kurohsu.dev/misc/webconf-2025-vue-sdd-ai-development.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/misc/webconf-2025-vue-sdd-ai-development.html</guid>
            <pubDate>Fri, 12 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="webconf-2025-延伸閱讀-vue-js-與-ai-協作的開發新思維" tabindex="-1">WebConf 2025 延伸閱讀：Vue.js 與 AI 協作的開發新思維 <a class="header-anchor" href="#webconf-2025-延伸閱讀-vue-js-與-ai-協作的開發新思維" aria-label="Permalink to “WebConf 2025 延伸閱讀：Vue.js 與 AI 協作的開發新思維”">&#8203;</a></h1>
<p>繼 <a href="/misc/jsdc-2025-ai-frontend-development.html">JSDC 2025</a> 之後，今天在 WebConf 2025 又講了一場「AI 只懂 React？Vue.js 也能 Vibe Coding！」。</p>
<p>事實上，演講標題裡的 React 只是個引子，<s>我知道你們想看什麼，但不是你們想的那樣 XD</s></p>
<p>這場演講真正想跟大家聊的，是 AI 協作時代下開發思維的轉變。
而最近在社群竄起的 SDD（Spec-Driven Development）剛好跟我一直以來的理念相當合拍，於是就有了這場延續 JSDC 的分享。</p>
<p>WebConf 當天雖然有將近 50 分鐘，但要把 SDD 從概念講到落地還是有點趕，很多實作細節只能快速帶過就跳下一張投影片，
趁著剛講完記憶還熱，我想補充一些沒講到的重點，並且把簡報重點整理一下，方便回顧。</p>
<p>廢話不多說，先上簡報：</p>
<iframe src="https://kuro-webconf-2025.vercel.app/" width="100%" height="500" style="border: 1px solid var(--vp-c-border); border-radius: 8px; margin: 1.5rem 0;" allowfullscreen />
<p style="text-align: center; margin-top: -0.5rem;">
  <a href="https://kuro-webconf-2025.vercel.app/" target="_blank">在新視窗開啟簡報 ↗</a>
</p>
<Callout type="info">
<p>本講演の <code>&lt;spec&gt;</code> のコンセプトは、<a href="https://x.com/ykoizumi0903" target="_blank" rel="noreferrer">@ykoizumi0903</a> さんの記事「<a href="https://zenn.dev/ytr0903/articles/2a3cc9dfba9945" target="_blank" rel="noreferrer">Vue SFC のカスタムブロックと AI 駆動開発の相性が良い理由</a>」から着想を得ました。</p>
<p>記事では、Spec-Driven Development の二つの課題として「仕様の言語化の難しさ」と「仕様とコードの分離による AI コンテキストの喪失」を指摘されています。Vue SFC の Custom Block を活用することで、仕様と実装を同一ファイルに配置し、AI が常に完全なコンテキストを参照できる点は、まさに目から鱗でした。</p>
<p>私はこの基盤の上に、いくつかの方向性を拡張しました：SDD の4ステップ閉ループフロー、逆同期（Distillation）の概念、レイヤー戦略（コア層/ビジネス層/プレゼンテーション層）、そして既存の CI/CD フローとの統合方法です。</p>
<p>この重要な洞察を共有してくださった ykoizumi0903 さんに心より感謝申し上げます。おかげで Vue コミュニティに具体的かつ実践可能な AI 協働のアプローチが生まれました。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="webconf-2025-延伸閱讀-vue-js-與-ai-協作的開發新思維" tabindex="-1">WebConf 2025 延伸閱讀：Vue.js 與 AI 協作的開發新思維 <a class="header-anchor" href="#webconf-2025-延伸閱讀-vue-js-與-ai-協作的開發新思維" aria-label="Permalink to “WebConf 2025 延伸閱讀：Vue.js 與 AI 協作的開發新思維”">&#8203;</a></h1>
<p>繼 <a href="/misc/jsdc-2025-ai-frontend-development.html">JSDC 2025</a> 之後，今天在 WebConf 2025 又講了一場「AI 只懂 React？Vue.js 也能 Vibe Coding！」。</p>
<p>事實上，演講標題裡的 React 只是個引子，<s>我知道你們想看什麼，但不是你們想的那樣 XD</s></p>
<p>這場演講真正想跟大家聊的，是 AI 協作時代下開發思維的轉變。
而最近在社群竄起的 SDD（Spec-Driven Development）剛好跟我一直以來的理念相當合拍，於是就有了這場延續 JSDC 的分享。</p>
<p>WebConf 當天雖然有將近 50 分鐘，但要把 SDD 從概念講到落地還是有點趕，很多實作細節只能快速帶過就跳下一張投影片，
趁著剛講完記憶還熱，我想補充一些沒講到的重點，並且把簡報重點整理一下，方便回顧。</p>
<p>廢話不多說，先上簡報：</p>
<iframe src="https://kuro-webconf-2025.vercel.app/" width="100%" height="500" style="border: 1px solid var(--vp-c-border); border-radius: 8px; margin: 1.5rem 0;" allowfullscreen />
<p style="text-align: center; margin-top: -0.5rem;">
  <a href="https://kuro-webconf-2025.vercel.app/" target="_blank">在新視窗開啟簡報 ↗</a>
</p>
<Callout type="info">
<p>本講演の <code>&lt;spec&gt;</code> のコンセプトは、<a href="https://x.com/ykoizumi0903" target="_blank" rel="noreferrer">@ykoizumi0903</a> さんの記事「<a href="https://zenn.dev/ytr0903/articles/2a3cc9dfba9945" target="_blank" rel="noreferrer">Vue SFC のカスタムブロックと AI 駆動開発の相性が良い理由</a>」から着想を得ました。</p>
<p>記事では、Spec-Driven Development の二つの課題として「仕様の言語化の難しさ」と「仕様とコードの分離による AI コンテキストの喪失」を指摘されています。Vue SFC の Custom Block を活用することで、仕様と実装を同一ファイルに配置し、AI が常に完全なコンテキストを参照できる点は、まさに目から鱗でした。</p>
<p>私はこの基盤の上に、いくつかの方向性を拡張しました：SDD の4ステップ閉ループフロー、逆同期（Distillation）の概念、レイヤー戦略（コア層/ビジネス層/プレゼンテーション層）、そして既存の CI/CD フローとの統合方法です。</p>
<p>この重要な洞察を共有してくださった ykoizumi0903 さんに心より感謝申し上げます。おかげで Vue コミュニティに具体的かつ実践可能な AI 協働のアプローチが生まれました。</p>
<hr>
<p>這場演講的 <code>&lt;spec&gt;</code> 概念，最初是受到 <a href="https://x.com/ykoizumi0903" target="_blank" rel="noreferrer">@ykoizumi0903</a> 的文章 <a href="https://zenn.dev/ytr0903/articles/2a3cc9dfba9945" target="_blank" rel="noreferrer">Vue SFC のカスタムブロックと AI 駆動開発の相性が良い理由</a> 啟發。</p>
<p>他在文中指出 Spec-Driven Development 的兩大痛點：規格難以語言化，以及規格與程式碼分離導致 AI 上下文遺失。而 Vue SFC 的 Custom Block 恰好能解決這個問題，將規格與實作放在同一個檔案，確保 AI 永遠能讀到完整的上下文。這個洞見讓我眼睛一亮。</p>
<p>我在這個基礎上延伸了幾個方向：SDD 的四步閉環流程、反向同步（Distillation）的概念、分層策略（核心層/業務層/展示層），以及與現有 CI/CD 流程的整合方式。
感謝 <a href="https://x.com/ykoizumi0903" target="_blank" rel="noreferrer">@ykoizumi0903</a> 提出這個關鍵洞見，讓 Vue 社群有了一個具體可行的 AI 協作方案。</p>
</Callout>
<p>這篇文章我想補充以下幾個重點：</p>
<ul>
<li><code>&lt;spec&gt;</code> 的環境設定與常見問題</li>
<li>有 Spec vs 無 Spec 的實際差異</li>
<li>SDD 落地時的限制與風險（對，任何方法論都有坑）</li>
<li>與現有 CI/CD 流程的整合方式</li>
<li>團隊導入的漸進式策略</li>
<li>會眾 FAQ（設計師 PM 參與、同步機制、跨元件狀態管理等）</li>
</ul>
<hr>
<h2 id="環境設定完整版" tabindex="-1">環境設定完整版 <a class="header-anchor" href="#環境設定完整版" aria-label="Permalink to “環境設定完整版”">&#8203;</a></h2>
<p>在開始使用 <code>&lt;spec&gt;</code> 之前，需要完成以下設定。</p>
<h3 id="_1-vite-plugin" tabindex="-1">1. Vite Plugin <a class="header-anchor" href="#_1-vite-plugin" aria-label="Permalink to “1. Vite Plugin”">&#8203;</a></h3>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// vite.config.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { defineConfig } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vite'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> vue </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@vitejs/plugin-vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ignoreSpecBlock</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'ignore-spec-block'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    transform</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">code</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">id</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (id.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'?vue&#x26;type=spec'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { code: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'export default {}'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  plugins: [</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(), </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ignoreSpecBlock</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br></div></div><p>這個 Plugin 的作用是：當 Vite 遇到 <code>&lt;spec&gt;</code> 區塊時，直接回傳空物件。Production build 不會包含任何 spec 內容，實現零執行成本。</p>
<h3 id="_2-typescript-宣告" tabindex="-1">2. TypeScript 宣告 <a class="header-anchor" href="#_2-typescript-宣告" aria-label="Permalink to “2. TypeScript 宣告”">&#8203;</a></h3>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// src/shims-spec.d.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">declare</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> module</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  interface</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> SFCCustomBlocksOptions</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">    spec</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>加上這個宣告，Volar 就不會對 <code>&lt;spec&gt;</code> 報紅字警告。</p>
<p>注意最後的 <code>export {}</code>：TypeScript 的 <code>.d.ts</code> 檔案如果沒有任何 <code>import</code> 或 <code>export</code>，會被視為 script（全域腳本）；加上 <code>export {}</code> 後才會被視為 module，此時 <code>declare module 'vue'</code> 才能正確作為 module augmentation 擴充 Vue 的型別定義。少了這行，<code>vue-tsc</code> 可能會報錯。</p>
<p>感謝 <a href="https://github.com/shunnNet" target="_blank" rel="noreferrer">ShunnNet</a> 留言指正！</p>
<h3 id="_3-ide-設定-vs-code-vue-official-extension-原-volar" tabindex="-1">3. IDE 設定（VS Code + Vue - Official Extension，原 Volar） <a class="header-anchor" href="#_3-ide-設定-vs-code-vue-official-extension-原-volar" aria-label="Permalink to “3. IDE 設定（VS Code + Vue - Official Extension，原 Volar）”">&#8203;</a></h3>
<p>在 <code>&lt;spec&gt;</code> 標籤加上 <code>lang=&quot;md&quot;</code>，可以獲得 Markdown 語法高亮：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">spec</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"md"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># UserCard</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Props</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user: { name: string, role: 'admin' | 'user' }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">spec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><h3 id="_4-eslint-設定-視情況加入" tabindex="-1">4. ESLint 設定（視情況加入） <a class="header-anchor" href="#_4-eslint-設定-視情況加入" aria-label="Permalink to “4. ESLint 設定（視情況加入）”">&#8203;</a></h3>
<p>通常 ESLint 會自動忽略未知的 Custom Block，不需要額外設定。</p>
<p>但如果你安裝了某些嚴格的 Parser 或 Markdown 檢查 Plugin 導致報錯，可以針對性調整。注意：<code>vue/comment-directive</code> 規則主要處理 <code>&lt;!-- eslint-disable --&gt;</code> 等註解，關閉它可能導致行內 ESLint 忽略功能失效。</p>
<div class="language-json line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "overrides"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "files"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"*.vue"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">      "rules"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 僅在遇到解析錯誤時才考慮加入</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // "vue/comment-directive": "off"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><p>建議先確認錯誤來源，再決定是否需要此設定。</p>
<hr>
<h2 id="無-spec-vs-有-spec-ai-產出的差異" tabindex="-1">無 Spec vs 有 Spec：AI 產出的差異 <a class="header-anchor" href="#無-spec-vs-有-spec-ai-產出的差異" aria-label="Permalink to “無 Spec vs 有 Spec：AI 產出的差異”">&#8203;</a></h2>
<p>讓 AI 實際寫一個 UserCard 元件，看看有沒有 Spec 的差異。</p>
<h3 id="情境-建立一個-usercard-元件" tabindex="-1">情境：建立一個 UserCard 元件 <a class="header-anchor" href="#情境-建立一個-usercard-元件" aria-label="Permalink to “情境：建立一個 UserCard 元件”">&#8203;</a></h3>
<p>需求：顯示使用者卡片，admin 要有特殊標示。</p>
<p><strong>無 Spec 的 Prompt：</strong></p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>幫我寫一個 UserCard 元件，顯示使用者名稱和角色，</span></span>
<span class="line"><span>admin 要有金色邊框。</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p><strong>AI 產出（可能結果）：</strong></p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> :class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"cardClass"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ user.name }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">span</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ user.role }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">span</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> props</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineProps</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'user'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">])</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> cardClass</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> computed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  props.user.role </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'admin'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'border-yellow-500'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> :</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br></div></div><p>問題：</p>
<ul>
<li><code>border-yellow-500</code> 而非設計稿的 <code>amber-400</code></li>
<li>沒有點擊事件</li>
<li>Props 沒有型別定義</li>
<li>每次生成可能不同</li>
</ul>
<hr>
<p><strong>有 Spec 的元件：</strong></p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">spec</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"md"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># UserCard</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Props</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user: { name: string, role: 'admin' | 'user' }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Behavior</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> role='admin' → 金色邊框 (amber-400)</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> role='user' → 預設邊框 (gray-200)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Interaction</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 點擊卡片觸發 </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">`select`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 事件，payload: user.id</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">spec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ts"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// AI 根據 Spec 生成...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><p><strong>AI 產出（穩定結果）：</strong></p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ts"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">interface</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> User</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  id</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  name</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  role</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'admin'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'user'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> props</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineProps</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;{ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> User</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }>()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> emit</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineEmits</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;{ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">select</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] }>()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> borderClass</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> computed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  props.user.role </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'admin'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    ?</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'border-amber-400'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    :</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'border-gray-200'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    :class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"['border-2', 'rounded-lg', 'p-4', borderClass]"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    @click</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"emit('select', user.id)"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ user.name }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">span</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ user.role }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">span</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br></div></div><h3 id="差異比較" tabindex="-1">差異比較 <a class="header-anchor" href="#差異比較" aria-label="Permalink to “差異比較”">&#8203;</a></h3>
<table tabindex="0">
<thead>
<tr>
<th>項目</th>
<th>無 Spec</th>
<th>有 Spec</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>一致性</strong></td>
<td>每次可能不同</td>
<td>穩定產出</td>
</tr>
<tr>
<td><strong>型別安全</strong></td>
<td>可能缺少</td>
<td>明確定義</td>
</tr>
<tr>
<td><strong>Design Token</strong></td>
<td>AI 自己猜</td>
<td>指定顏色</td>
</tr>
<tr>
<td><strong>事件定義</strong></td>
<td>可能遺漏</td>
<td>明確列出</td>
</tr>
<tr>
<td><strong>可維護性</strong></td>
<td>改需求要重新描述</td>
<td>改 Spec 即可</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="為什麼不用-jsdoc-或註解就好" tabindex="-1">為什麼不用 JSDoc 或註解就好？ <a class="header-anchor" href="#為什麼不用-jsdoc-或註解就好" aria-label="Permalink to “為什麼不用 JSDoc 或註解就好？”">&#8203;</a></h2>
<p>可能有人會問：「為什麼不直接用 JSDoc 或程式碼註解來寫 Spec 就好？為什麼要用 <code>&lt;spec&gt;</code> Custom Block？」
這是一個好問題。讓我們比較一下兩者的差異：</p>
<table tabindex="0">
<thead>
<tr>
<th>比較項目</th>
<th>JSDoc / 註解</th>
<th><code>&lt;spec&gt;</code> Custom Block</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>結構化程度</strong></td>
<td>純文字，散落在程式碼各處</td>
<td>Markdown 結構（## Props, ## Behavior）</td>
</tr>
<tr>
<td><strong>可定位性</strong></td>
<td>AI 需要在 <code>//</code> 註解中猜測哪些是規格</td>
<td>AI 精準定位 <code>&lt;spec&gt;</code> 標籤</td>
</tr>
<tr>
<td><strong>意圖明確性</strong></td>
<td>可能是實作註解，也可能是說明</td>
<td>標籤本身表示「這是規格」</td>
</tr>
<tr>
<td><strong>IDE 支援</strong></td>
<td>無特殊處理</td>
<td>可加 <code>lang=&quot;md&quot;</code> 獲得語法高亮</td>
</tr>
</tbody>
</table>
<p><strong>核心差異</strong>：在 Vue SFC 中，<code>&lt;spec&gt;</code> 提供了一個「明確標示為規格」的獨立區塊，AI 不需要猜測哪些註解是規格、哪些只是實作說明。</p>
<p>更重要的是 <strong>Co-location（空間群聚）</strong> 的價值：我們利用 Vue SFC 的特性，將 Spec 與 Code 放在同一個檔案，確保 AI 在處理該元件時，必定能讀取到這份「最高權重的指令」。
這比依賴外部文檔（容易過期、容易被 AI 忽略）或散落在各處的註解更可靠，同時也更易於人工後續維護。</p>
<p>當然，JSDoc 在非 SFC 的場景（如 Store、工具函式、composables）仍然是很好的選擇。
這裡我認為重點不在 「JSDoc vs <code>&lt;spec&gt;</code>」 的兩者對立，而是不同層級下怎麼選用最適合的方式。</p>
<Callout type="info">
<p>LLM 對結構化的 Markdown 遵循能力較高。就像交接工作時，給同事一份條列式 SOP，比在程式碼裡到處寫註解更有效率。</p>
</Callout>
<hr>
<h2 id="spec-寫作準則" tabindex="-1">Spec 寫作準則 <a class="header-anchor" href="#spec-寫作準則" aria-label="Permalink to “Spec 寫作準則”">&#8203;</a></h2>
<h3 id="該寫什麼" tabindex="-1">該寫什麼 <a class="header-anchor" href="#該寫什麼" aria-label="Permalink to “該寫什麼”">&#8203;</a></h3>
<table tabindex="0">
<thead>
<tr>
<th>類別</th>
<th>說明</th>
<th>範例</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Props 資料契約</strong></td>
<td>輸入的資料結構與型別</td>
<td><code>user: { name: string, role: 'admin' | 'user' }</code></td>
</tr>
<tr>
<td><strong>Behavior 行為規則</strong></td>
<td>條件與結果的對應</td>
<td><code>role='admin' → 金色邊框 (amber-400)</code></td>
</tr>
<tr>
<td><strong>Interaction 互動定義</strong></td>
<td>使用者操作與事件</td>
<td><code>點擊觸發 select 事件，payload 為 user.id</code></td>
</tr>
<tr>
<td><strong>視覺規範 Design Token</strong></td>
<td>顏色、間距等設計值</td>
<td><code>amber-400</code>、<code>gap-4</code>、<code>rounded-lg</code></td>
</tr>
</tbody>
</table>
<h3 id="不該寫什麼" tabindex="-1">不該寫什麼 <a class="header-anchor" href="#不該寫什麼" aria-label="Permalink to “不該寫什麼”">&#8203;</a></h3>
<table tabindex="0">
<thead>
<tr>
<th>類別</th>
<th>錯誤範例</th>
<th>為什麼不該寫</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>程式庫選擇</strong></td>
<td><code>使用 axios.get 獲取資料</code></td>
<td>屬於專案層（CLAUDE.md）</td>
</tr>
<tr>
<td><strong>實作細節</strong></td>
<td><code>使用 ref&lt;boolean&gt;(false) 儲存 isOpen</code></td>
<td>換實作方式就要改</td>
</tr>
<tr>
<td><strong>佈局細節</strong></td>
<td><code>使用 flex justify-center 置中</code></td>
<td>這是 CSS 實作</td>
</tr>
<tr>
<td><strong>錯誤處理機制</strong></td>
<td><code>用 try-catch 包住 API 呼叫</code></td>
<td>可能與全域處理衝突</td>
</tr>
</tbody>
</table>
<h3 id="判斷原則" tabindex="-1">判斷原則 <a class="header-anchor" href="#判斷原則" aria-label="Permalink to “判斷原則”">&#8203;</a></h3>
<p>兩個問題幫你判斷 Spec 粒度：</p>
<ol>
<li><strong>「換一種實作方式，這段描述需要改嗎？」</strong> → 要改就是太細了</li>
<li><strong>「AI 看完這段，還需要問我問題嗎？」</strong> → 會問就是太粗了</li>
</ol>
<hr>
<h2 id="design-token-保留的重要性" tabindex="-1">Design Token 保留的重要性 <a class="header-anchor" href="#design-token-保留的重要性" aria-label="Permalink to “Design Token 保留的重要性”">&#8203;</a></h2>
<p>反向同步時，很多人會把「看起來像實作」的東西都移除，結果把 <code>amber-400</code> 也移除了。</p>
<p><strong>這是錯誤的。</strong></p>
<p>Design Token 不是實作細節，是<strong>視覺規範</strong>。它們是 AI 保持 UI 一致性的關鍵。</p>
<div class="language-markdown line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># 錯誤：移除了 Design Token</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Behavior</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> admin 使用者顯示金色邊框</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># 正確：保留 Design Token</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Behavior</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> role='admin' → 金色邊框 (amber-400)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><p>只寫「金色邊框」，AI 可能生成 <code>border-yellow-500</code>、<code>border-amber-300</code>、甚至 <code>border-[#ffd700]</code>。寫明 <code>amber-400</code>，AI 就知道用這個特定顏色。</p>
<hr>
<h2 id="sdd-的限制與風險" tabindex="-1">SDD 的限制與風險 <a class="header-anchor" href="#sdd-的限制與風險" aria-label="Permalink to “SDD 的限制與風險”">&#8203;</a></h2>
<p>任何方法論都有適用範圍。<s>不然就變成邪教了</s>（笑）。誠實說明這些限制，才能幫你做正確決策。</p>
<h3 id="已知限制" tabindex="-1">已知限制 <a class="header-anchor" href="#已知限制" aria-label="Permalink to “已知限制”">&#8203;</a></h3>
<table tabindex="0">
<thead>
<tr>
<th>限制</th>
<th>緩解方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>團隊需要時間適應「先寫 Spec」</td>
<td>漸進式導入，從新元件開始</td>
</tr>
<tr>
<td>Spec 寫太詳細會變成負擔</td>
<td>遵守「只寫意圖」原則</td>
</tr>
<tr>
<td>跨元件情境粒度難定義</td>
<td>以「獨立可測試的單位」為粒度</td>
</tr>
<tr>
<td>不同 AI 工具處理不一致</td>
<td>使用規則檔同步機制</td>
</tr>
</tbody>
</table>
<h3 id="風險警示" tabindex="-1">風險警示 <a class="header-anchor" href="#風險警示" aria-label="Permalink to “風險警示”">&#8203;</a></h3>
<h4 id="spec-與-code-衝突" tabindex="-1">Spec 與 Code 衝突 <a class="header-anchor" href="#spec-與-code-衝突" aria-label="Permalink to “Spec 與 Code 衝突”">&#8203;</a></h4>
<p><strong>案例</strong>：Spec 寫「用 try-catch 並 alert」，但專案引入了 Axios Interceptor。AI 照 Spec 生成 try-catch，錯誤訊息跳兩次。</p>
<p><strong>解法</strong>：Spec 不寫實作機制，只寫意圖（「處理異常，遵循全域規範」）。</p>
<h4 id="測試過度生成" tabindex="-1">測試過度生成 <a class="header-anchor" href="#測試過度生成" aria-label="Permalink to “測試過度生成”">&#8203;</a></h4>
<p>讓 AI 同時生成 Code 和 Test，可能產生大量「為測試而測試」的案例。</p>
<p><strong>解法</strong>：在 Spec 中標註「需要測試的關鍵行為」，使用分層策略。</p>
<h4 id="過度依賴-spec" tabindex="-1">過度依賴 Spec <a class="header-anchor" href="#過度依賴-spec" aria-label="Permalink to “過度依賴 Spec”">&#8203;</a></h4>
<p>團隊花太多時間寫 Spec，反而減緩開發。</p>
<p><strong>解法</strong>：記住目標是「讓 AI 寫出可控的程式碼」，不是「寫完美的文件」。Spec 夠用就好。</p>
<hr>
<h2 id="與現有開發流程整合" tabindex="-1">與現有開發流程整合 <a class="header-anchor" href="#與現有開發流程整合" aria-label="Permalink to “與現有開發流程整合”">&#8203;</a></h2>
<h3 id="ci-cd-pipeline" tabindex="-1">CI/CD Pipeline <a class="header-anchor" href="#ci-cd-pipeline" aria-label="Permalink to “CI/CD Pipeline”">&#8203;</a></h3>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>[開發] Spec（人工）→ AI CodeGen → AI TestGen → 本地驗證</span></span>
<span class="line"><span>[提交] git commit → Pre-commit Hook（檢查 Spec 是否更新）</span></span>
<span class="line"><span>[CI]   PR 建立 → Vitest → Lint → Type Check</span></span>
<span class="line"><span>[Review] 先看 &#x3C;spec> 變更 → 再看 Code → 確認 Test 覆蓋</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><h3 id="pr-review-checklist" tabindex="-1">PR Review Checklist <a class="header-anchor" href="#pr-review-checklist" aria-label="Permalink to “PR Review Checklist”">&#8203;</a></h3>
<p>在 PR Template 加入：</p>
<div class="language-markdown line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## SDD Checklist</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 新增元件是否已加上 </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">`&#x3C;spec>`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">？</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 修改行為時 </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">`&#x3C;spec>`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 是否已同步？</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 測試是否覆蓋 </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">`&#x3C;spec>`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 定義的關鍵行為？</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><h3 id="git-hooks-自動提醒-node-js-腳本範例" tabindex="-1">Git Hooks 自動提醒（Node.js 腳本範例） <a class="header-anchor" href="#git-hooks-自動提醒-node-js-腳本範例" aria-label="Permalink to “Git Hooks 自動提醒（Node.js 腳本範例）”">&#8203;</a></h3>
<p>將此腳本存為 <code>scripts/check-spec.js</code>，並在 <code>.husky/pre-commit</code> 中以 <code>node scripts/check-spec.js</code> 執行：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// scripts/check-spec.js</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">execSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'child_process'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> changedFiles</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> execSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'git diff --cached --name-only'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">split</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">changedFiles</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">f</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> f.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">endsWith</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">forEach</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">file</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> diff</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> execSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`git diff --cached ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">file</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> hasCodeChanges</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> diff.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x3C;script'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> diff.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x3C;template'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> hasSpecChanges</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> diff.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x3C;spec'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (hasCodeChanges </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">hasSpecChanges) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">warn</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`⚠️ ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">file</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}: 修改了程式碼但沒更新 &#x3C;spec>`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><p><strong>注意</strong>：此腳本較為嚴格。在純重構（Refactoring）不改變行為的情況下，可能會出現誤報。建議團隊約定在 commit message 加入特定 tag（如 <code>[refactor]</code>）來略過此檢查，或將腳本改為僅輸出提醒而非阻擋 commit。</p>
<hr>
<h2 id="反向同步-distillation-實戰指南" tabindex="-1">反向同步（Distillation）實戰指南 <a class="header-anchor" href="#反向同步-distillation-實戰指南" aria-label="Permalink to “反向同步（Distillation）實戰指南”">&#8203;</a></h2>
<p>當你修改了程式碼但忘記更新 Spec，或者接手一個沒有 Spec 的舊元件，就需要「反向同步」：從程式碼萃取出 Spec。
這裡的重點是抽象出「意圖」，而非「實作細節」。</p>
<h3 id="實際操作步驟" tabindex="-1">實際操作步驟 <a class="header-anchor" href="#實際操作步驟" aria-label="Permalink to “實際操作步驟”">&#8203;</a></h3>
<p><strong>Step 1：給 AI 的 Prompt</strong></p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>請閱讀這個元件的實作，萃取出 &#x3C;spec> 區塊。</span></span>
<span class="line"><span></span></span>
<span class="line"><span>規則：</span></span>
<span class="line"><span>1. 只寫「意圖」，不寫「實作方式」</span></span>
<span class="line"><span>2. 保留 Design Token（如 amber-400）</span></span>
<span class="line"><span>3. 使用 Props / Behavior / Interaction 三段式結構</span></span>
<span class="line"><span>4. 移除任何程式庫相關描述（如 axios、pinia）</span></span>
<span class="line"><span>5. 如果程式碼中包含可疑邏輯或潛在 Bug，請標註為 (待確認)，不要直接視為正規行為</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p><strong>Step 2：AI 產出的初稿</strong></p>
<p>假設元件程式碼是：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ts"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { ref, computed } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useUserStore } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@/stores/user'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">interface</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Props</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  userId</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  showBadge</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> boolean</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> props</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> withDefaults</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">defineProps</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Props</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(), {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  showBadge: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> emit</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineEmits</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;{</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  click</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">userId</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  delete</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">userId</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}>()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> userStore</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useUserStore</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> computed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> userStore.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getUserById</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(props.userId))</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> isAdmin</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> computed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.value?.role </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'admin'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"p-4 rounded-lg border-2"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    :class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"isAdmin ? 'border-amber-400 bg-amber-50' : 'border-gray-200'"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    @click</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"emit('click', userId)"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">span</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"showBadge &#x26;&#x26; isAdmin"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"badge-admin"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>Admin&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">span</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ user?.name }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> @click.stop</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"emit('delete', userId)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>刪除&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br></div></div><p>AI 萃取結果：</p>
<div class="language-markdown line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># UserCard</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Props</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> userId: string - 使用者 ID</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> showBadge: boolean (default: true) - 是否顯示角色徽章</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Behavior</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 根據 userId 從 store 取得使用者資料</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> role='admin' → 金色邊框 (amber-400) + 淺金背景 (amber-50)</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> role='admin' 且 showBadge=true → 顯示 Admin 徽章</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 其他角色 → 灰色邊框 (gray-200)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Interaction</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 點擊卡片 → 觸發 </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">`click`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 事件，payload: userId</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 點擊刪除按鈕 → 觸發 </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">`delete`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 事件，payload: userId（不冒泡）</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br></div></div><p><strong>Step 3：人工審查與調整</strong></p>
<p>AI 萃取的 Spec 需要人工確認：</p>
<ul>
<li>Design Token 有保留</li>
<li>事件定義完整</li>
<li>「根據 userId 從 store 取得」可能太實作導向，改成「根據 userId 顯示對應使用者」</li>
</ul>
<h3 id="反向同步能自動化嗎" tabindex="-1">反向同步能自動化嗎？ <a class="header-anchor" href="#反向同步能自動化嗎" aria-label="Permalink to “反向同步能自動化嗎？”">&#8203;</a></h3>
<p><strong>短答案：半自動化可行，全自動化風險高。</strong></p>
<h4 id="可自動化的部分" tabindex="-1">可自動化的部分 <a class="header-anchor" href="#可自動化的部分" aria-label="Permalink to “可自動化的部分”">&#8203;</a></h4>
<ol>
<li><strong>偵測變更</strong>：Git Hook 可以偵測「程式碼改了但 Spec 沒改」</li>
<li><strong>生成初稿</strong>：AI 可以從程式碼萃取 Spec 草稿</li>
<li><strong>格式檢查</strong>：確認 Spec 包含必要區塊（Props / Behavior / Interaction）</li>
</ol>
<h4 id="不建議自動化的部分" tabindex="-1">不建議自動化的部分 <a class="header-anchor" href="#不建議自動化的部分" aria-label="Permalink to “不建議自動化的部分”">&#8203;</a></h4>
<ol>
<li><strong>直接覆蓋 Spec</strong>：AI 萃取可能包含實作細節，需要人工過濾</li>
<li><strong>語意判斷</strong>：「這是意圖還是實作？」需要人類判斷</li>
<li><strong>業務邏輯確認</strong>：AI 不知道某個行為是 Bug 還是 Feature</li>
</ol>
<h4 id="建議的半自動化流程" tabindex="-1">建議的半自動化流程 <a class="header-anchor" href="#建議的半自動化流程" aria-label="Permalink to “建議的半自動化流程”">&#8203;</a></h4>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># .husky/pre-commit（提醒模式）</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># 注意：這是概念範例，實際使用請根據專案現況調整</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">#!/bin/sh</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">changed_vue_files</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> diff</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --cached</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --name-only</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> grep</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '\.vue$'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> file </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">in</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $changed_vue_files; </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">do</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  # 檢查是否有 &#x3C;spec> 區塊</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> grep</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -q</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '&#x3C;spec'</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$file</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">then</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "⚠️  </span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$file</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 沒有 &#x3C;spec> 區塊"</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    continue</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  fi</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  # 檢查 spec 是否有更新</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  diff</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> diff</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --cached</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$file</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  has_code_change</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$diff</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> grep</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -E</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '&#x3C;script|&#x3C;template'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  has_spec_change</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$diff</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> grep</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '&#x3C;spec'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$has_code_change</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ] &#x26;&#x26; [ </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-z</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$has_spec_change</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ]; </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">then</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "⚠️  </span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$file</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">: 程式碼有變更但 &#x3C;spec> 沒更新"</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "   執行 'pnpm spec:sync </span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$file</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">' 生成建議"</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  fi</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">done</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br></div></div><div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// scripts/spec-sync.js（生成建議，不自動覆蓋）</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Anthropic </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@anthropic-ai/sdk'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> fs </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'fs'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> client</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Anthropic</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> generateSpecSuggestion</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">filePath</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> content</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">readFileSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(filePath, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'utf-8'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> message</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> client.messages.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    model: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'claude-sonnet-4-20250514'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    max_tokens: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1024</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    messages: [{</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      role: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'user'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`請從這個 Vue 元件萃取 &#x3C;spec>，只寫意圖不寫實作：</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">content</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'=== 建議的 Spec ==='</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(message.content[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].text)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">請人工審查後貼入元件'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br></div></div><h4 id="為什麼不建議全自動化" tabindex="-1">為什麼不建議全自動化？ <a class="header-anchor" href="#為什麼不建議全自動化" aria-label="Permalink to “為什麼不建議全自動化？”">&#8203;</a></h4>
<p>反向同步的核心價值在於「思考這段程式碼的意圖是什麼」。</p>
<p>如果完全自動化：</p>
<ul>
<li>Spec 會變成「程式碼的另一種描述」而非「意圖的規格」</li>
<li>失去人類審查，錯誤的行為會被記錄成「正確的 Spec」</li>
<li>Bug 會被當成 Feature 寫進 Spec</li>
</ul>
<Callout type="warning">
<p><strong>最佳實踐</strong>：自動化偵測 + AI 生成建議 + 人工審查確認。</p>
<p>記住，Spec 是給 AI 的「工作指令」，不是給版控的「程式碼描述」。讓機器做機器擅長的事，人做人擅長的事。</p>
</Callout>
<hr>
<h2 id="決策樹-該用-sdd-嗎" tabindex="-1">決策樹：該用 SDD 嗎？ <a class="header-anchor" href="#決策樹-該用-sdd-嗎" aria-label="Permalink to “決策樹：該用 SDD 嗎？”">&#8203;</a></h2>
<p>不是每個元件都需要 Spec。<s>不然光寫 Spec 就飽了</s>。這裡提供一個簡單的決策流程：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>這個元件需要寫 &#x3C;spec> 嗎？</span></span>
<span class="line"><span>│</span></span>
<span class="line"><span>├─ 跟「錢」有關？（金流、付款、訂單）</span></span>
<span class="line"><span>│   └─ YES → Spec + Test</span></span>
<span class="line"><span>│</span></span>
<span class="line"><span>├─ 會被多處重用？</span></span>
<span class="line"><span>│   └─ YES → Spec Only</span></span>
<span class="line"><span>│</span></span>
<span class="line"><span>├─ 有複雜的條件邏輯？</span></span>
<span class="line"><span>│   └─ YES → Spec Only</span></span>
<span class="line"><span>│</span></span>
<span class="line"><span>├─ 需要長期維護？</span></span>
<span class="line"><span>│   └─ YES → Spec Only</span></span>
<span class="line"><span>│</span></span>
<span class="line"><span>└─ 以上皆否 → 直接 Vibe Coding</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br></div></div><h3 id="元件分類參考" tabindex="-1">元件分類參考 <a class="header-anchor" href="#元件分類參考" aria-label="Permalink to “元件分類參考”">&#8203;</a></h3>
<table tabindex="0">
<thead>
<tr>
<th>元件類型</th>
<th>策略</th>
<th>Test</th>
<th>理由</th>
</tr>
</thead>
<tbody>
<tr>
<td>PaymentForm</td>
<td>Spec + Test</td>
<td>Yes</td>
<td>金流，錯誤代價高</td>
</tr>
<tr>
<td>LoginForm</td>
<td>Spec + Test</td>
<td>Yes</td>
<td>權限，安全性重要</td>
</tr>
<tr>
<td>UserCard</td>
<td>Spec Only</td>
<td>Optional</td>
<td>可重用，但非核心</td>
</tr>
<tr>
<td>DataTable</td>
<td>Spec Only</td>
<td>Optional</td>
<td>複雜互動</td>
</tr>
<tr>
<td>PageHeader</td>
<td>Vibe Coding</td>
<td>No</td>
<td>純展示</td>
</tr>
<tr>
<td>AboutPage</td>
<td>Vibe Coding</td>
<td>No</td>
<td>一次性頁面</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="團隊導入策略" tabindex="-1">團隊導入策略 <a class="header-anchor" href="#團隊導入策略" aria-label="Permalink to “團隊導入策略”">&#8203;</a></h2>
<p>說真的，最難的不是技術，是說服團隊。「又來一個新東西要學？」這種反應很正常。</p>
<h3 id="漸進式導入時程" tabindex="-1">漸進式導入時程 <a class="header-anchor" href="#漸進式導入時程" aria-label="Permalink to “漸進式導入時程”">&#8203;</a></h3>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>Phase 1（第 1-2 週）</span></span>
<span class="line"><span>└─ 只在「新元件」使用 SDD</span></span>
<span class="line"><span>└─ 指定 1-2 位 Champion 先試水溫</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Phase 2（第 3-4 週）</span></span>
<span class="line"><span>└─ 重構舊元件時順便加 Spec</span></span>
<span class="line"><span>└─ 建立 Spec 範本，降低撰寫門檻</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Phase 3（第 5 週起）</span></span>
<span class="line"><span>└─ PR Review 開始檢查 Spec</span></span>
<span class="line"><span>└─ 每週五花 30 分鐘，AI 批次檢查同步狀態</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><h3 id="spec-範本" tabindex="-1">Spec 範本 <a class="header-anchor" href="#spec-範本" aria-label="Permalink to “Spec 範本”">&#8203;</a></h3>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">spec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"># [元件名稱]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">## Props</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">- prop1: type - 說明</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">## Behavior</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">- 條件 → 結果 (design-token)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">## Interaction</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">- 動作觸發 `event-name` 事件，payload: { ... }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">spec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><hr>
<h2 id="會眾-faq" tabindex="-1">會眾 FAQ <a class="header-anchor" href="#會眾-faq" aria-label="Permalink to “會眾 FAQ”">&#8203;</a></h2>
<p>演講結束後收到不少問題，這裡整理幾個比較常見的，順便補充一些在簡報裡來不及講的內容。</p>
<h3 id="q1-設計師和-pm-需要參與-spec-撰寫嗎" tabindex="-1">Q1: 設計師和 PM 需要參與 Spec 撰寫嗎？ <a class="header-anchor" href="#q1-設計師和-pm-需要參與-spec-撰寫嗎" aria-label="Permalink to “Q1: 設計師和 PM 需要參與 Spec 撰寫嗎？”">&#8203;</a></h3>
<blockquote>
<p>還是純粹是工程師的工作？</p>
</blockquote>
<p>我的看法是：<strong>Spec 的撰寫責任在工程師，但內容來源不只是工程師</strong>。</p>
<p>設計師和 PM 不需要學會寫 <code>&lt;spec&gt;</code> 語法，但他們提供的資訊會直接影響 Spec 的品質：</p>
<table tabindex="0">
<thead>
<tr>
<th>角色</th>
<th>貢獻</th>
<th>範例</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>設計師</strong></td>
<td>Design Token、視覺規範</td>
<td>「這個按鈕用 amber-400，不是 yellow-500」</td>
</tr>
<tr>
<td><strong>PM</strong></td>
<td>業務邏輯、Edge Cases</td>
<td>「admin 可以刪除，但要二次確認」</td>
</tr>
<tr>
<td><strong>工程師</strong></td>
<td>整合成結構化 Spec</td>
<td>把上述資訊寫成 Props / Behavior / Interaction</td>
</tr>
</tbody>
</table>
<p>實務上的做法是：工程師寫完 Spec 初稿後，拉設計師和 PM 快速 Review 一下「這是你們要的嗎？」。這個過程通常 5 分鐘就結束，但可以避免後面來回修改。</p>
<p>如果你的團隊設計師或 PM 比較積極，也可以讓他們直接在 Spec 上面用註解補充，工程師再整理成正式格式。重點是「資訊要流通」，誰來寫只是執行細節。</p>
<hr>
<h3 id="q2-spec-跟-code-不同步怎麼辦-有沒有強制機制" tabindex="-1">Q2: Spec 跟 Code 不同步怎麼辦？有沒有強制機制？ <a class="header-anchor" href="#q2-spec-跟-code-不同步怎麼辦-有沒有強制機制" aria-label="Permalink to “Q2: Spec 跟 Code 不同步怎麼辦？有沒有強制機制？”">&#8203;</a></h3>
<blockquote>
<p>講者提到「趕時間直接改 Code 忘記更新 Spec」是現實，有沒有強制機制防止這種情況？</p>
</blockquote>
<p>有，但我建議「提醒優先，阻擋其次」。</p>
<p>文章前面有提到 Git Hook 的做法，這裡補充幾個層級：</p>
<p><strong>Level 1：提醒（推薦先從這裡開始）</strong></p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># pre-commit hook 只 warn，不阻擋</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">echo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "⚠️ 這些 .vue 檔案改了程式碼但沒更新 &#x3C;spec>，請確認是否需要同步"</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p>好處是不會打斷開發節奏，讓工程師自己判斷「這次改動是否影響行為」。純重構、改變數名稱這種不改行為的修改，本來就不需要更新 Spec。</p>
<p><strong>Level 2：PR Review 檢查</strong></p>
<p>在 PR Template 加入 Checklist：</p>
<div class="language-markdown line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 行為有變更時，</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">`&#x3C;spec>`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 已同步更新</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>這是人工檢查，但有紀錄可追蹤。</p>
<p><strong>Level 3：CI 阻擋（謹慎使用）</strong></p>
<p>可以在 CI 裡跑一個 script 檢查「有改 <code>&lt;script&gt;</code> 或 <code>&lt;template&gt;</code> 但沒改 <code>&lt;spec&gt;</code>」的檔案，然後 fail 掉。但這會有誤報（重構不改行為的情況），所以要搭配 escape hatch，例如在 commit message 加 <code>[no-spec]</code> 可以跳過檢查。</p>
<p><strong>我的建議</strong></p>
<p>從 Level 1 開始，觀察一兩週看看漏掉的頻率。如果團隊自律性夠高，Level 1 就夠了；如果經常漏，再升級到 Level 2。Level 3 通常只有在「這個元件出錯代價很高」（例如金流相關）才需要。</p>
<p><s>話說，如果工程師連提醒都無視，那問題可能不在工具上</s>（笑）</p>
<hr>
<h3 id="q3-ai-反覆修正會不會比自己寫還慢-什麼時候該放棄" tabindex="-1">Q3: AI 反覆修正會不會比自己寫還慢？什麼時候該放棄？ <a class="header-anchor" href="#q3-ai-反覆修正會不會比自己寫還慢-什麼時候該放棄" aria-label="Permalink to “Q3: AI 反覆修正會不會比自己寫還慢？什麼時候該放棄？”">&#8203;</a></h3>
<blockquote>
<p>如果 AI 生成的結果需要反覆修正，會不會比自己寫還慢？</p>
</blockquote>
<p>會，而且這是真實會發生的情況。</p>
<p>我自己的判斷標準是「<strong>三次原則</strong>」：第一次 AI 產出不對，先調整 Prompt 或 Spec；第二次還是不對，檢查是不是 Context 不夠，Rules 檔案有沒有相關規則；如果第三次還是不對，就直接手寫吧。</p>
<p>三次修正的時間，通常已經超過自己寫的時間了。繼續跟 AI 糾纏下去只會更浪費時間。</p>
<p><strong>什麼情況 AI 特別容易卡住？</strong></p>
<table tabindex="0">
<thead>
<tr>
<th>情境</th>
<th>原因</th>
<th>建議</th>
</tr>
</thead>
<tbody>
<tr>
<td>複雜的狀態邏輯</td>
<td>AI 難以追蹤多層狀態變化</td>
<td>自己寫核心邏輯，AI 寫周邊</td>
</tr>
<tr>
<td>專案特有的 Pattern</td>
<td>AI 沒見過你們的寫法</td>
<td>先在 Rules 放範例</td>
</tr>
<tr>
<td>Edge Case 處理</td>
<td>AI 傾向 Happy Path</td>
<td>手動補充邊界條件</td>
</tr>
<tr>
<td>效能敏感的程式碼</td>
<td>AI 不知道你的效能瓶頸</td>
<td>自己寫，AI 只做 Review</td>
</tr>
</tbody>
</table>
<p><strong>換個角度想</strong></p>
<p>「AI 生成 → 修正 → 再生成」這個過程，其實也是在幫你釐清需求。有時候 AI 寫錯，反而讓你發現「原來我自己也沒想清楚這邊要怎麼處理」。</p>
<p>所以即使最後選擇手寫，這個過程也不算完全浪費。<s>只是心情會有點煩躁</s></p>
<hr>
<h3 id="q4-跨元件的複雜互動怎麼處理" tabindex="-1">Q4: 跨元件的複雜互動怎麼處理？ <a class="header-anchor" href="#q4-跨元件的複雜互動怎麼處理" aria-label="Permalink to “Q4: 跨元件的複雜互動怎麼處理？”">&#8203;</a></h3>
<blockquote>
<p>簡報展示的是單一元件的 Spec，如果是跨多個元件的狀態管理（例如 Pinia），Spec 要怎麼寫？</p>
</blockquote>
<p>這是個好問題，因為 <code>&lt;spec&gt;</code> 的設計確實是以「單一元件」為單位。跨元件的情況需要用不同的方式處理。</p>
<p><strong>我的分層策略</strong></p>
<table tabindex="0">
<thead>
<tr>
<th>層級</th>
<th>放哪裡</th>
<th>內容</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>專案層</strong></td>
<td><code>CLAUDE.md</code></td>
<td>Store 的整體架構、命名規範</td>
</tr>
<tr>
<td><strong>模組層</strong></td>
<td>Store 檔案的 JSDoc</td>
<td>這個 Store 的職責、對外 API</td>
</tr>
<tr>
<td><strong>元件層</strong></td>
<td><code>&lt;spec&gt;</code></td>
<td>這個元件如何使用 Store</td>
</tr>
</tbody>
</table>
<p><strong>實際範例</strong></p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// stores/cart.ts</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@module</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> CartStore</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> *</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * ## 職責</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * - 管理購物車商品列表</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * - 計算總價（含折扣邏輯）</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * - 與後端同步購物車狀態</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> *</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * ## 對外 API</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * - items: 商品列表（唯讀）</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * - totalPrice: 總價（含折扣）</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * - addItem(product, quantity): 加入商品</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * - removeItem(productId): 移除商品</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * - checkout(): 結帳，回傳 orderId</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> *</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * ## 狀態流</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * 1. addItem → 更新 items → 觸發 totalPrice 重算</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * 2. checkout → 呼叫 API → 清空 items → 回傳 orderId</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> useCartStore</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineStore</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'cart'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br></div></div><p>然後在元件的 <code>&lt;spec&gt;</code> 裡這樣寫：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">spec</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"md"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold"># CartButton</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Dependencies</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 使用 CartStore 的 items 和 addItem</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## Behavior</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> 點擊 → 呼叫 CartStore.addItem(product, 1)</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> items.length > 0 → 顯示紅點數字</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">spec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><p><strong>為什麼不把所有東西都寫在元件 Spec 裡？</strong></p>
<p>因為會重複。如果 10 個元件都用到 CartStore，你不會想在 10 個地方都寫一次 Store 的完整規格。</p>
<p>元件的 Spec 只需要說明「這個元件怎麼用這個 Store」，Store 本身的規格放在 Store 檔案的 JSDoc 或獨立文件。</p>
<hr>
<h3 id="q5-團隊成員技術水平不一-怎麼確保-spec-品質一致" tabindex="-1">Q5: 團隊成員技術水平不一，怎麼確保 Spec 品質一致？ <a class="header-anchor" href="#q5-團隊成員技術水平不一-怎麼確保-spec-品質一致" aria-label="Permalink to “Q5: 團隊成員技術水平不一，怎麼確保 Spec 品質一致？”">&#8203;</a></h3>
<blockquote>
<p>講者提到 Spec 寫太細或太粗都有問題，有沒有具體的 review checklist？</p>
</blockquote>
<p>有，這是我目前實驗中整理出來的 Spec Review Checklist，供參考：</p>
<p><strong>Spec Review Checklist</strong></p>
<div class="language-markdown line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## 結構完整性</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 有 Props 區塊（如果元件接收 props）</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 有 Behavior 區塊（描述條件與結果）</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 有 Interaction 區塊（如果有使用者互動）</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## 粒度檢查</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 沒有寫到「用什麼函式/API」（太細）</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 沒有寫到「怎麼實作」（太細）</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 有寫到「Design Token」（視覺規範要保留）</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 看完 Spec 後，AI 不需要再問問題（太粗的話會需要）</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold">## 品質檢查</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 使用「條件 → 結果」格式描述行為</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 事件有說明 payload 結構</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ ] 沒有模稜兩可的描述（如「適當處理」「必要時」）</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br></div></div><p><strong>實務上怎麼推動？</strong></p>
<p>先從範本開始，前面文章有提供 Spec 範本，讓團隊成員照著填空就好。前幾次一起 Review，資深成員帶著 Junior 一起看，解釋為什麼這樣寫。然後建立 Bad Examples，收集「寫太細」和「寫太粗」的反例，讓大家知道邊界在哪。</p>
<p><strong>一個簡單的判斷口訣</strong></p>
<blockquote>
<p><strong>「換一種寫法，Spec 要改嗎？」</strong></p>
<ul>
<li>要改 → 太細了，刪掉</li>
<li>不用改 → 可以保留</li>
</ul>
</blockquote>
<p>比如「使用 <code>ref&lt;boolean&gt;(false)</code>」要改成「使用 <code>reactive</code>」的話 Spec 就要改，所以這句太細了。
但「admin 顯示金色邊框 (amber-400)」換成用 CSS Variable 實作，Spec 也不用改，所以可以保留。</p>
<hr>
<h3 id="q6-既有專案怎麼辦" tabindex="-1">Q6: 既有專案怎麼辦？ <a class="header-anchor" href="#q6-既有專案怎麼辦" aria-label="Permalink to “Q6: 既有專案怎麼辦？”">&#8203;</a></h3>
<blockquote>
<p>我們現有的 Vue 專案沒有 <code>&lt;spec&gt;</code> 區塊，要從哪裡開始導入？全面重寫還是漸進式？</p>
</blockquote>
<p><strong>絕對不要全面重寫</strong>。<s>除非你想讓團隊集體崩潰</s></p>
<p>漸進式導入的策略：</p>
<p><strong>Phase 1：只對新元件使用</strong></p>
<p>從今天開始，所有新建立的元件都加上 <code>&lt;spec&gt;</code>。舊的不動。</p>
<p>這是最低成本的起步方式，團隊可以先熟悉 Spec 的寫法，累積經驗。</p>
<p><strong>Phase 2：修 Bug 時順便加</strong></p>
<p>當你要修某個舊元件的 Bug，先花 5 分鐘讓 AI 幫你反向同步（Distillation）出 Spec 初稿，人工審核後加進去。</p>
<p>這樣 Spec 會隨著日常維護慢慢覆蓋到舊程式碼，而且是從「最常被改動的元件」開始，ROI 最高。</p>
<p><strong>Phase 3：重點元件優先</strong></p>
<p>如果想更主動一點，可以列出專案裡的「核心元件」，像是被很多地方引用的共用元件、跟金流或權限相關的元件、經常出 Bug 的元件，這些優先加上 Spec + Test。</p>
<p><strong>實際時程建議</strong></p>
<table tabindex="0">
<thead>
<tr>
<th>週數</th>
<th>目標</th>
</tr>
</thead>
<tbody>
<tr>
<td>1-2 週</td>
<td>新元件 100% 有 Spec，指定 1-2 人當 Champion</td>
</tr>
<tr>
<td>3-4 週</td>
<td>建立 Spec 範本和 Review Checklist，開始在 PR Review 檢查</td>
</tr>
<tr>
<td>5-8 週</td>
<td>逐步對核心元件補 Spec（每週 2-3 個）</td>
</tr>
<tr>
<td>之後</td>
<td>維持習慣，隨日常維護自然覆蓋</td>
</tr>
</tbody>
</table>
<p><strong>千萬不要做的事</strong></p>
<p>不要一次性把所有元件都補上 Spec，浪費時間而且品質不會好。也不要強制要求兩週內全部完成，團隊會反彈。更不要沒有 Champion 就開始推，你需要有人負責回答問題、做示範。</p>
<p><strong>心態調整</strong></p>
<p>Spec 覆蓋率不是目標，<strong>「讓 AI 產出可控」才是目標</strong>。</p>
<p>如果某個元件本來就很穩定、很少改動，沒有 Spec 也不會怎樣。把精力放在「會被頻繁改動」和「出錯代價高」的元件上。</p>
<hr>
<h2 id="結語" tabindex="-1">結語 <a class="header-anchor" href="#結語" aria-label="Permalink to “結語”">&#8203;</a></h2>
<p>寫這篇文章的過程中，我一直在想一件事：SDD 的核心價值到底是什麼？</p>
<p>不是讓 AI 寫出更多程式碼，而是讓我們<strong>用更少的時間產出更可控的結果</strong>。</p>
<p>Spec 本質上是一種「意圖的結晶」。當你把腦中模糊的需求寫成結構化的 Spec，其實就是在強迫自己想清楚「我到底要什麼」。這個過程本身就有價值，就算不用 AI，這份 Spec 也能幫助團隊溝通、幫助 Code Review、幫助未來的自己回想當初的設計意圖。</p>
<p>說到底，<strong>Spec 的本質是溝通</strong>。它不只是寫給 AI 看的提示詞，也是寫給人類接手者看的規格書。即使哪天不用 AI 了，良好的規格描述本來就是高品質程式碼的一部分。</p>
<p>我在演講的時候也提到，工程師天生就討厭寫文件，也討厭別人不寫文件。<s>然後自己不看文件，也討厭別人不看文件。</s>
這個部分正好由 AI 來幫忙承擔。只要我們願意花點時間「先寫 Spec」，AI 就能幫我們把這些 Spec 轉化成程式碼和測試，減少人類撰寫的負擔。</p>
<p>雖然流程增加了「撰寫規格」的成本，本質上是用「前期思考時間」來換取「後期 Debug 時間」。
這筆交易是否划算，我認為取決於團隊的專案複雜度與團隊的成員素質。</p>
<p>對於快速迭代的原型期專案，這可能是負擔；但對於需要長期維護的核心業務，這筆投資通常是值得的。</p>
<p>所以與其說 SDD 是一種「AI 開發方法論」，不如說它是一種<strong>思考方式的轉變</strong>：從「我要寫什麼程式碼」變成「我要表達什麼意圖」。</p>
<p>SDD 某種程度上是針對「當前 AI 能力限制」所設計的工程解法。隨著 AI 持續進步，未來或許不需要這麼明確的結構化 Spec。但 Spec-first 的思維方式本身有獨立價值，它迫使我們在動手之前先想清楚意圖，這個習慣不會因為工具進步而過時。</p>
<p>這個轉變不容易，但值得嘗試。</p>
<hr>
<h2 id="相關資源" tabindex="-1">相關資源 <a class="header-anchor" href="#相關資源" aria-label="Permalink to “相關資源”">&#8203;</a></h2>
<ul>
<li><a href="https://kuro-webconf-2025.vercel.app" target="_blank" rel="noreferrer">簡報連結</a></li>
<li><a href="https://vuejs.org/guide/scaling-up/sfc.html#custom-blocks" target="_blank" rel="noreferrer">Vue SFC Custom Blocks 官方文件</a></li>
<li><a href="https://vitest.dev/" target="_blank" rel="noreferrer">Vitest 官方文件</a></li>
<li><a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview" target="_blank" rel="noreferrer">Anthropic Prompt Engineering</a></li>
</ul>
<p>如有問題或建議，歡迎留言討論！</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue</category>
            <category>ai</category>
            <category>sdd</category>
            <category>vibe-coding</category>
            <category>webconf</category>
        </item>
        <item>
            <title><![CDATA[JSDC 2025 演講回顧：讓 AI 寫前端，我們來寫未來]]></title>
            <link>https://kurohsu.dev/misc/jsdc-2025-ai-frontend-development.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/misc/jsdc-2025-ai-frontend-development.html</guid>
            <pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="jsdc-2025-演講回顧-讓-ai-寫前端-我們來寫未來" tabindex="-1">JSDC 2025 演講回顧：讓 AI 寫前端，我們來寫未來 <a class="header-anchor" href="#jsdc-2025-演講回顧-讓-ai-寫前端-我們來寫未來" aria-label="Permalink to “JSDC 2025 演講回顧：讓 AI 寫前端，我們來寫未來”">&#8203;</a></h1>
<p>終於結束了！ 上週六很榮幸能在 JSDC 2025 跟大家分享「讓 AI 寫前端，我們來寫未來」這個主題。
老實說，雖然我從第一屆 JSDC 上台到現在已經十多年了，但還是第一次講這麼不 JavaScript 的主題（笑）</p>
<p><s>除了蹭一下 AI 熱潮</s>，這場演講想探討的，就是在 AI 工具越來越強大的今天，
我們前端工程師該如何與 AI 協作，避免掉入「Vibe Coding」的陷阱，同時又能善用 AI 的生產力，提升開發效率與品質。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="jsdc-2025-演講回顧-讓-ai-寫前端-我們來寫未來" tabindex="-1">JSDC 2025 演講回顧：讓 AI 寫前端，我們來寫未來 <a class="header-anchor" href="#jsdc-2025-演講回顧-讓-ai-寫前端-我們來寫未來" aria-label="Permalink to “JSDC 2025 演講回顧：讓 AI 寫前端，我們來寫未來”">&#8203;</a></h1>
<p>終於結束了！ 上週六很榮幸能在 JSDC 2025 跟大家分享「讓 AI 寫前端，我們來寫未來」這個主題。
老實說，雖然我從第一屆 JSDC 上台到現在已經十多年了，但還是第一次講這麼不 JavaScript 的主題（笑）</p>
<p><s>除了蹭一下 AI 熱潮</s>，這場演講想探討的，就是在 AI 工具越來越強大的今天，
我們前端工程師該如何與 AI 協作，避免掉入「Vibe Coding」的陷阱，同時又能善用 AI 的生產力，提升開發效率與品質。</p>
<hr>
<h2 id="關於-jsdc-2025" tabindex="-1">關於 JSDC 2025 <a class="header-anchor" href="#關於-jsdc-2025" aria-label="Permalink to “關於 JSDC 2025”">&#8203;</a></h2>
<p><a href="https://jsdc.tw/" target="_blank" rel="noreferrer">JSDC</a>（JavaScript Developer Conference）是台灣最大的 JavaScript 年度技術研討會，自 2011 年起由多個開發者社群共同發起，致力於提供台灣中高階 JavaScript 技術人才與世界最新技術交流的平台，整合獨立開發者、企業與組織的技術力量。</p>
<p>今年的 <a href="https://2025.jsdc.tw/" target="_blank" rel="noreferrer">JSDC 2025</a> 於 11 月 29 日在集思台大會議中心舉辦，主題是「<strong>Code Compose Connect</strong>」，聚焦在生成式工具與智慧化開發流程的發展，探討開發者如何透過協作重新定義創作的邊界。</p>
<img src="/images/misc/jsdc-2025.png" alt="JSDC 2025" class="my-12 rounded-lg shadow-lg" />
<p>今年 JSDC 最大的感觸就是：<strong>AI 真的無所不在</strong>。
不只是我的場次，我聽其他講者分享時，大家都不約而同地聊到 AI 如何改變開發流程，工具鏈，甚至是產品設計思維。
這種氛圍讓我深刻感受到，AI 已經不再是「未來的趨勢」，而是「現在進行式」的現實。</p>
<hr>
<h2 id="演講內容回顧" tabindex="-1">演講內容回顧 <a class="header-anchor" href="#演講內容回顧" aria-label="Permalink to “演講內容回顧”">&#8203;</a></h2>
<p>在演講一開始，我做了一個簡單的田野調查：「現場有多少人已經在用 AI 輔助開發？」</p>
<p>結果目測有<strong>九成以上</strong>的人舉手。</p>
<p>這個數字其實不意外，但親眼看到還是蠻震撼的。 AI 輔助開發現在已經不是「要不要用」的問題，而是「怎麼用才不會踩坑」的問題。</p>
<h3 id="什麼是-vibe-coding" tabindex="-1">什麼是 Vibe Coding？ <a class="header-anchor" href="#什麼是-vibe-coding" aria-label="Permalink to “什麼是 Vibe Coding？”">&#8203;</a></h3>
<p>接著我用一個場景帶入：「三分鐘用 AI 生成一個電商產品頁」。
畫面看起來很漂亮，但打開 Console 一看，滿滿的紅字錯誤。</p>
<p>這就是所謂的 <strong>Vibe Coding</strong>：憑感覺寫 Code。 戴上耳機，感覺對了，Code 生成了，畫面出來了，看起來很美。
但身為工程師的我們都知道，<strong>看起來能動跟真的能上線，中間隔著一道巨大的鴻溝</strong>。表面上省了時間，底層卻埋了更多技術債，填坑填不完。</p>
<h3 id="六個開發階段的-ai-協作陷阱" tabindex="-1">六個開發階段的 AI 協作陷阱 <a class="header-anchor" href="#六個開發階段的-ai-協作陷阱" aria-label="Permalink to “六個開發階段的 AI 協作陷阱”">&#8203;</a></h3>
<p>我把前端開發拆成六個階段，分析每個階段 Vibe Coding 容易踩的坑，以及如何避免：</p>
<table tabindex="0">
<thead>
<tr>
<th>階段</th>
<th>Vibe Coding 的坑</th>
<th>解法</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Phase 1: 需求分析</strong></td>
<td>AI 只處理 Happy Path，Edge Cases 得靠人想</td>
<td><strong>Interface First</strong>：先定義資料結構和元件介面</td>
</tr>
<tr>
<td><strong>Phase 2: 視覺切版</strong></td>
<td>AI 亂猜顏色、間距、圓角，造成「視覺負債」</td>
<td><strong>Design Tokens</strong>：綁進 Tailwind，限制 AI 只能用定義的值</td>
</tr>
<tr>
<td><strong>Phase 3: 元件實作</strong></td>
<td>AI 幻覺（漏 <code>.value</code>、混用 API、過時語法）</td>
<td><strong>Rules 檔案</strong>：.cursorrules 或 CLAUDE.md</td>
</tr>
<tr>
<td><strong>Phase 4: 前後端整合</strong></td>
<td>只寫 Happy Path，不處理 Loading/Error/Race Condition</td>
<td><strong>OpenAPI + MCP</strong>：提供即時規格和 Checklist</td>
</tr>
<tr>
<td><strong>Phase 5: 測試與品質</strong></td>
<td>測試只測 Happy Path，Mock 太多沒意義</td>
<td><strong>TDD 思維</strong>：人寫測試定義正確，AI 寫實作</td>
</tr>
<tr>
<td><strong>Phase 6: 效能與部署</strong></td>
<td>「能跑」不等於「能上線」</td>
<td><strong>Pre-deployment Checklist</strong>：Lighthouse、Bundle Size、敏感資訊檢查</td>
</tr>
</tbody>
</table>
<h2 id="prompt-engineering-已死-context-engineering-當立" tabindex="-1">Prompt Engineering 已死？Context Engineering 當立！ <a class="header-anchor" href="#prompt-engineering-已死-context-engineering-當立" aria-label="Permalink to “Prompt Engineering 已死？Context Engineering 當立！”">&#8203;</a></h2>
<p>演講後半段，我提出一個觀點：重點不是「怎麼問」（Prompt Engineering），而是「給什麼」（Context Engineering）。</p>
<h3 id="什麼是-context-engineering" tabindex="-1">什麼是 Context Engineering？ <a class="header-anchor" href="#什麼是-context-engineering" aria-label="Permalink to “什麼是 Context Engineering？”">&#8203;</a></h3>
<p>根據 <a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" target="_blank" rel="noreferrer">Anthropic 的定義</a>，Context 指的是「在對 LLM 取樣時所包含的所有 token」，而 Context Engineering 就是「針對 LLM 固有的限制，優化這些 token 的效用」。<a href="https://blog.langchain.com/the-rise-of-context-engineering/" target="_blank" rel="noreferrer">LangChain 的文章</a> 則給了更實務的定義：</p>
<blockquote>
<p>Context Engineering 是建構動態系統，以正確的格式提供正確的資訊和工具，讓 LLM 能夠合理地完成任務。</p>
</blockquote>
<p>簡單來說，Prompt Engineering 關注「怎麼問」，用詞彙和技巧調整 AI 行為；Context Engineering 關注「給什麼」，用結構化的資訊架構引導 AI。</p>
<p><strong>為什麼 Context Engineering 更重要？</strong></p>
<p>大多數 AI Agent 失敗的原因，不是模型能力不足，而是<strong>沒有提供適當的背景資訊給模型</strong>。</p>
<p>LLM 有一個特性叫 <strong>Context Rot</strong>：隨著 Context Window 中的 token 數量增加，模型回憶資訊的準確度會下降。Context 是有限資源，不是塞越多越好，而是要精準地提供「對的資訊」。AI 的智商取決於模型，但 AI 的<strong>準確度</strong>取決於 Context。</p>
<h3 id="前端開發的四種-context" tabindex="-1">前端開發的四種 Context <a class="header-anchor" href="#前端開發的四種-context" aria-label="Permalink to “前端開發的四種 Context”">&#8203;</a></h3>
<p>在演講中，我把 Context Engineering 對應到前端開發的四個階段，整理成四種 Context 類型：</p>
<p><strong>Data Context（資料的形狀）</strong></p>
<p>對應<strong>需求分析</strong>階段。就是 TypeScript 的 Types 和 Interfaces。</p>
<p>還記得前面講的嗎？先定義 <code>Product</code> interface，裡面有 <code>status</code> 欄位，AI 就知道要處理缺貨狀態。資料的形狀決定了 AI 要處理什麼，這就是「給方向，不給答案」，你定義結構，AI 填內容。</p>
<p><strong>Visual Context（視覺的邊界）</strong></p>
<p>對應<strong>視覺切版</strong>階段。就是 Design Tokens。</p>
<p>把顏色、間距、圓角都定義好綁進 Tailwind，AI 就只能在這個範圍內選擇。這是「限制選擇範圍」的概念，與其寫一堆「不要用 #ff0000」，不如直接定義「只能用這幾個顏色」。限制選擇，就是提高準確度。</p>
<p><strong>Constraint Context（規則與禁令）</strong></p>
<p>對應<strong>元件實作</strong>階段。就是 <code>.cursorrules</code> 或 <code>CLAUDE.md</code>。</p>
<p>告訴 AI：<code>NEVER use any</code>、<code>ALWAYS use Composition API</code>、<code>記得加 .value</code>。明確的禁止和要求，讓 AI 知道邊界在哪。這裡「給範例比給規則有效」，與其寫一堆規則，不如在 Rules 檔案裡放幾個標準的元件範例讓 AI 參考。</p>
<p><strong>Knowledge Context（即時的規格與知識）</strong></p>
<p>對應<strong>前後端整合</strong>階段。這裡有兩個面向：</p>
<p>一個是 <strong>API 規格</strong>。如果有 OpenAPI (Swagger) spec，AI 可以根據正確的 API 合約生成 Client Code，不用你一個一個欄位跟它講。這是跟後端協作的基礎。</p>
<p>另一個是<strong>即時知識</strong>。AI 的訓練資料可能停在一年前，它不知道 Vue 3.5 可以解構 props。透過 MCP，AI 可以即時查詢最新文件、資料庫 Schema，用對的方式寫 Code。</p>
<p>還沒導入 MCP 也沒關係，先把 <code>openapi.yaml</code> 或 <code>schema.sql</code> 放在專案的 <code>docs/</code> 資料夾，在規則檔裡指引 AI 去讀，也是一個好的開始。</p>
<Callout type="info">
<p>為什麼叫「工程」而不是「技巧」？因為這是系統性的方法。你不是在賭 AI 這次會不會寫對，而是在建構一個環境，讓 AI 在這個環境裡面只能寫對。</p>
<p>這四個 Context 剛好對應到前面講的四個開發階段，這不是巧合。
<strong>AI 時代的前端架構，其實就是在為 AI 建構一個高品質的 Context 環境。</strong></p>
</Callout>
<hr>
<h2 id="會眾-q-a-補充" tabindex="-1">會眾 Q&amp;A 補充 <a class="header-anchor" href="#會眾-q-a-補充" aria-label="Permalink to “會眾 Q&amp;A 補充”">&#8203;</a></h2>
<p>由於時間關係，在現場沒能進行 Q&amp;A，所以我在這裡整理幾個會眾透過 Slido 提出的問題。</p>
<h3 id="q1-design-token-感覺是設計師的工作-前端該怎麼辦" tabindex="-1">Q1: Design Token 感覺是設計師的工作，前端該怎麼辦？ <a class="header-anchor" href="#q1-design-token-感覺是設計師的工作-前端該怎麼辦" aria-label="Permalink to “Q1: Design Token 感覺是設計師的工作，前端該怎麼辦？”">&#8203;</a></h3>
<blockquote>
<p>在讓 AI 寫前端畫面時，定義 design token 或畫面相關要給的規格感覺是設計師的工作，我們身為前端工程師該怎麼自己通靈規格，或改怎麼跟設計師要求規格？或是我們身為前端工程師，有推薦該如何學習哪些設計概念以加強 AI 的產出嗎？</p>
</blockquote>
<p>很多前端會覺得「這是設計師的事吧？」，但現實是，很多時候我們根本沒有設計師，或者設計師沒空理你。這時候前端自己把 Token 定義起來，其實是在救自己。</p>
<p>如果你有設計師，可以主動拿著 Design Token 的概念去跟他聊，說明這樣做可以讓開發更一致、修改更方便。很多設計師其實樂見這種系統化的做法，只是不知道怎麼開始。Figma 本身就有 Variables 功能可以定義 Design Token。</p>
<p>如果設計稿已經給了，那就自己整理出重複使用的顏色、間距、圓角。你甚至可以用 AI 幫你分析：「請從這份設計稿中整理出常用的顏色、間距、字體大小」。</p>
<p>另外推薦了解一些基本設計概念：8px Grid System（間距用 8 的倍數）、Typography Scale（字體大小有規律的比例）、Color System（主色、輔色、語意色）。不需要變成設計師，但這些知識能讓你跟設計師溝通更順暢。</p>
<p>實在不想自己搞的話，Tailwind 本身的預設值就是經過設計的系統，直接用 <code>text-lg</code>、<code>p-4</code>、<code>rounded-md</code> 這些 class，已經比亂猜數值好很多了。</p>
<hr>
<h3 id="q2-ai-可以幫忙統合不同團隊的程式碼嗎" tabindex="-1">Q2: AI 可以幫忙統合不同團隊的程式碼嗎？ <a class="header-anchor" href="#q2-ai-可以幫忙統合不同團隊的程式碼嗎" aria-label="Permalink to “Q2: AI 可以幫忙統合不同團隊的程式碼嗎？”">&#8203;</a></h3>
<blockquote>
<p>假設一個官網有英文版和中文版，是個別由不同團隊開發的網站，所以開發方法、初始定義的樣式、元件模組都是個別刻的。但後來希望能盡量統合成一致、同步規格，以便加快復刻的範例。定義成一致的規格當作 design token 給 AI 去復刻？</p>
</blockquote>
<p>可以，但別想一步到位。</p>
<p>我會建議這樣做：先讓 AI 分別讀兩邊的程式碼，整理出各自的命名慣例、元件結構、樣式寫法。這一步是為了搞清楚「差異在哪」。</p>
<p>接下來，「要統一成哪一種」這件事得由人來決定。哪邊的寫法比較好維護？哪邊的團隊比較大需要遷就？有沒有第三種更好的方式？這些 AI 沒辦法幫你判斷。</p>
<p>決定好之後，把統一的 Design Token、元件 Props 介面、Coding Style 寫成規格文件，這份文件就會成為 AI 的 Context。</p>
<p>最後是漸進式重構。不要想一次全改，從新功能開始用統一規格，舊的部分有碰到再逐步調整就好。</p>
<p>不過說真的，這種跨團隊的統合，溝通成本往往比技術成本高。AI 可以幫你產出程式碼，但「讓兩個團隊同意用同一套規格」這件事，還是得靠人去喬。</p>
<hr>
<h3 id="q3-實務上很難全部做到-技術債持續增加怎麼辦" tabindex="-1">Q3: 實務上很難全部做到，技術債持續增加怎麼辦？ <a class="header-anchor" href="#q3-實務上很難全部做到-技術債持續增加怎麼辦" aria-label="Permalink to “Q3: 實務上很難全部做到，技術債持續增加怎麼辦？”">&#8203;</a></h3>
<blockquote>
<p>概念上能理解講者講的各個原則、執行要注意的細項，但是實務上覺得還是很難全部做到，尤其目前專案已經越來越多技術債增加的感覺。想聽講者對實務上克服的看法。</p>
</blockquote>
<p>講是講「理想狀態」，但我自己也沒辦法 100% 做到（笑）。重點不是追求完美，而是「不要讓新的債繼續堆上去」。</p>
<p>如果現在專案一團亂，不可能一夜之間變好。 我的做法是「每次碰到的程式碼，離開時比來時乾淨一點」就好。
舊的技術債先放著，但新功能開始導入 Interface First、Design Token 這些做法，至少新的部分不會繼續堆債。</p>
<p>要挑的話，先挑 ROI 高的做。
像是建立 Rules 檔案（<code>.cursorrules</code> 或 <code>CLAUDE.md</code>），當我們規則寫得好，寫一次就能讓 AI 每次都遵守，投資報酬率超高。
Design Token 也是，建立一次之後效益是累積的。</p>
<p>另外，技術債很多時候來自「團隊沒有共識」。與其靠 Code Review 抓問題，不如把規則寫進 ESLint、寫進 AI Rules，用工具自動化的約束比口頭約定可靠多了。</p>
<p>還有，AI 很擅長做「把所有 <code>var</code> 改成 <code>const</code>」、「把 Options API 改成 Composition API」 之類的重構，
搭配 MCP 讓 AI 去讀官方文件，AI 可以幫你把舊程式碼升級到新標準，最後我們只要驗收就好。</p>
<p>這樣就可以把人力放在更高價值的地方，讓 AI 當苦力，人專注在架構決策就好。</p>
<hr>
<h3 id="q4-有些規則是-ai-產出後才發現需要的-怎麼辦" tabindex="-1">Q4: 有些規則是 AI 產出後才發現需要的，怎麼辦？ <a class="header-anchor" href="#q4-有些規則是-ai-產出後才發現需要的-怎麼辦" aria-label="Permalink to “Q4: 有些規則是 AI 產出後才發現需要的，怎麼辦？”">&#8203;</a></h3>
<blockquote>
<p>規格要在 AI 產出前先寫清楚，避免規則後補，但有些細節有可能 AI 產出後才發現我希望遵循某些規則或細節，算是一種不理解自己需求或是無法清楚表達所有細節的狀況？請問這狀況要如何改善？</p>
</blockquote>
<p>這完全正常，不需要太焦慮。規則本來就是「迭代」出來的，不可能一開始就想到所有細節。</p>
<p>我的做法是：AI 產出一段程式碼，發現「欸，這邊應該要怎樣怎樣」，就立刻把這個規則加進 Rules 檔案，然後再讓 AI 讀取新的 Rules，重新產出。
這樣不斷累積規則，AI 寫出來的東西就會越來越符合團隊需求。</p>
<p>我們可以建立一份「AI 踩過的坑」清單，每次 AI 做錯什麼就記下來變成規則。這份清單會越來越完整，最後就是你團隊專屬的 Best Practice。</p>
<p>其實這是一個學習過程：你對 AI 的理解在加深，AI 對你專案的理解也在累積（透過 Rules 檔案）。前幾次會比較痛苦，後面會越來越順。</p>
<p>不要覺得這是「不理解自己需求」。很多細節本來就是做了才會浮現，傳統開發也一樣，
寫著寫著才發現「啊，這邊需要處理 xxx 狀況」，差別只是以前自己寫自己改，現在是跟 AI 協作而已。</p>
<hr>
<h3 id="q5-只會寫-code-的工程師是否岌岌可危" tabindex="-1">Q5: 只會寫 code 的工程師是否岌岌可危？ <a class="header-anchor" href="#q5-只會寫-code-的工程師是否岌岌可危" aria-label="Permalink to “Q5: 只會寫 code 的工程師是否岌岌可危？”">&#8203;</a></h3>
<blockquote>
<p>看起來用 AI coding 最重要的是讓它能理解需求、想要的工具以及目的。但這樣來說，coding 對我們工程師來說已經不是最重要的，而是「釐清」。也想請教說，是不是「只會寫 code」的工程師未來是不是岌岌可危，或是我們該要有怎麼樣的 mindset？</p>
</blockquote>
<p>這題我想拆開來聊。</p>
<p><strong>「會寫 code」變成基本功，而不是全部價值</strong></p>
<p>把「會寫 code」比喻成「會用 Word」，聽起來有點誇張，但想講的是：以前「把需求轉成程式碼」這個轉譯過程本身就很稀有、有價值。現在有 AI，「把自然語言變成程式碼」這一段越來越可以被工具代勞，但「寫什麼、為什麼要這樣寫、整體系統要長什麼樣子」還是人決定。</p>
<p>所以 code 變成「你跟 AI 溝通的語言」和「你驗收成果的工具」，而不是你唯一的產出。但這不代表你可以不會寫 code，不會寫 code，基本上很難「管得住」AI 寫出來的東西。</p>
<p><strong>真正的價值在「AI 不擅長」的地方</strong></p>
<p><strong>釐清問題 / 需求分析</strong>：AI 可以幫忙整理、brainstorm，但真正的需求來自商業目標、限制、風險、時間、預算，這些是「政治 + 商業 + 人性」的綜合產物。AI 可以記住、推理已知的規則，但它不會主動 challenge「這個需求有意義嗎？」、不會問「這個 KPI 設對了嗎？」、沒有「政治後果意識」與「長期責任感」。能清楚說出「我們到底在解什麼問題？不做這功能會怎樣？」這個能力超值錢。</p>
<p><strong>系統 / 架構設計</strong>：AI 很會「給你某個檔案的程式碼」，但「整個系統要拆幾個 service？資料流怎麼走？未來容易改嗎？安全性？擴充性？」這些高維度的 trade-off，牽涉到團隊能力、既有系統、部署環境、組織文化。這種「設計空間的決策」，AI 可以給選項，但最後要你扛責任。</p>
<p><strong>品質把關</strong>：包含 code review、測試策略、觀察指標、風險評估。AI 可以幫你寫測試、生成 code，但「測試有沒有測到重點？這個改動會不會踩到別的系統？這個實作在高流量、高錯誤率情境下撐得住嗎？」這些是經驗 + 系統性思維，不是單純語言模型就能搞定的。</p>
<p><strong>溝通協調</strong>：AI 可以幫很多「低層工作」，像是整理會議重點、把技術說明翻成 PM 聽得懂的話、列 pro/cons 當溝通素材。但真正難的是：判斷「現在這個場要講真話還是講安慰版」、在衝突中拿捏立場、願意站出來說「現在的做法會爆，大家要不要一起調整？」這種「關係 + 風險 + 誠實度」的東西，靠的是人。</p>
<p><strong>一個重要前提：你本來就要有不錯的工程基礎</strong></p>
<p>「大家都來當管 AI 的人」這句話有個現實前提：你要管得動 AI，基本盤還是要讀得懂 code、看得出異常、知道什麼是壞味道（code smell）、懂基本的設計、測試、部署。不然就會變成在「相信 AI」vs「更相信 AI」之間做選擇，而不是「判斷它到底對不對」。</p>
<p>如果有人解讀成「那我就不用學那麼扎實的 coding 了，反正以後 AI 會寫」，那就會悲劇。比較健康的理解是：學 code 的目的，從「自己一行一行刻」變成「能快速理解、重構、組合、把關」。</p>
<p>不要害怕 AI，把它當成一個很會寫 code 但需要你指導的 Junior 就好。未來的決勝點在於：你能不能駕馭 AI，幫你寫出高品質的系統。</p>
<hr class="!my-16" />
<p>這場演講的核心訊息是：AI 是強大的工具，但工具需要人來駕馭。</p>
<p>Vibe Coding 的問題不在於用 AI，而在於「無腦地用」。當我們建立好 Interface、Design Token、Rules 檔案這些 Context，AI 的產出品質會大幅提升。</p>
<p><strong>前端沒有死，死掉的是舊的開發模式。</strong></p>
<p>我常跟團隊說，雖然 AI 幫我們寫 code，但每一次 Commit, PR 都是自己的名字在背書，當 Git Blame 被靠北的時候躲也躲不掉。</p>
<p>業界在談 Responsible AI，講的是倫理、公平、透明。但對我們開發者來說，「負責任地使用 AI」更實際的意思是：<strong>AI 幫你寫，你為它的產出買單</strong>。
不是不用，而是要用得明白、用得有把握，對最終上線的每一行 code 負責。</p>
<p>從前的前端，我們會強調我們是「橋接設計與後端的拱心石」，現在的前端有了 AI 的幫助，前端的價值早就不只是「把頁面刻出來」而已。 當我們把效能做到極致，使用者留存就會提高；當我們用動畫、視覺化、沉浸式體驗串聯情緒，使用者對品牌的記憶就會更深；當我們讓 AI 與產品互動自然融合，整個產品就會打開新的成長空間。</p>
<p>現在的前端生態，有框架、有工具、有 AI 輔助，從 idea 發想到上線可以快到不可思議。
如果你想深入創新，有 Edge Runtime、有 Streaming UI、有 AI 原生架構，讓你在未知領域挖掘自己的金礦。</p>
<p>我認為 <strong>AI 與前端的交會點，就是未來最大的機會。</strong></p>
<p>這是我們與 AI 共存的最好時代，也是最考驗架構能力的時代。</p>
<img src="/images/misc/jsdc-2025-ai-frontend-development.jpg" alt="JSDC 2025 演講現場照片" class="my-8 rounded-lg shadow-lg" />
<p>感謝 JSDC 2025 的邀請，以及現場所有參與的會眾。期待明年再見！</p>
<p><strong>簡報連結：</strong></p>
<ul>
<li><a href="https://kuro-jsdc-2025-en.vercel.app" target="_blank" rel="noreferrer">English ver.</a></li>
<li><a href="https://kuro-jsdc-2025.vercel.app" target="_blank" rel="noreferrer">繁體中文版</a></li>
</ul>
<hr>
<p>最後預告一下</p>
<Callout type="warning">
<p>12 月我在 <a href="https://webconf.tw/" target="_blank" rel="noreferrer">WebConf 2025</a> 也有一場 「<a href="https://webconf.tw/agenda/38" target="_blank" rel="noreferrer"><strong>AI 只懂 React？Vue.js 也能 Vibe Coding！</strong></a>」的演講，這場的內容預計會是這場的延伸，更聚焦在實務上 Vue.js 生態系如何與 AI 協作，以及 Vue.js 開發者該注意的 AI 陷阱，歡迎有興趣的朋友來聽！</p>
</Callout>
<p>然後再預告一下 😏</p>
<img src="/images/misc/v-conf.jpeg" alt="v-conf 2026" class="my-6 rounded-lg shadow-lg" />
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>AI</category>
            <category>前端開發</category>
            <category>Vue.js</category>
            <category>JSDC</category>
        </item>
        <item>
            <title><![CDATA[為 VitePress 部落格加上 Vercel Analytics]]></title>
            <link>https://kurohsu.dev/notes/integrating-vercel-analytics-to-vitepress.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/integrating-vercel-analytics-to-vitepress.html</guid>
            <pubDate>Wed, 12 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="為-vitepress-部落格加上-vercel-analytics" tabindex="-1">為 VitePress 部落格加上 Vercel Analytics <a class="header-anchor" href="#為-vitepress-部落格加上-vercel-analytics" aria-label="Permalink to “為 VitePress 部落格加上 Vercel Analytics”">&#8203;</a></h1>
<p>部落格上線一陣子後，總會好奇有多少人來看、哪些文章比較受歡迎、網站跑起來順不順。
原本在考慮要不要裝 Google Analytics，但想到它那肥大的 script 和 Cookie 同意的麻煩，就覺得有點煩。
後來發現 Vercel Analytics 超輕量又不用處理 Cookie 問題，整合起來也簡單，這篇就來記錄一下實作過程。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="為-vitepress-部落格加上-vercel-analytics" tabindex="-1">為 VitePress 部落格加上 Vercel Analytics <a class="header-anchor" href="#為-vitepress-部落格加上-vercel-analytics" aria-label="Permalink to “為 VitePress 部落格加上 Vercel Analytics”">&#8203;</a></h1>
<p>部落格上線一陣子後，總會好奇有多少人來看、哪些文章比較受歡迎、網站跑起來順不順。
原本在考慮要不要裝 Google Analytics，但想到它那肥大的 script 和 Cookie 同意的麻煩，就覺得有點煩。
後來發現 Vercel Analytics 超輕量又不用處理 Cookie 問題，整合起來也簡單，這篇就來記錄一下實作過程。</p>
<hr>
<h2 id="為什麼選-vercel-analytics" tabindex="-1">為什麼選 Vercel Analytics？ <a class="header-anchor" href="#為什麼選-vercel-analytics" aria-label="Permalink to “為什麼選 Vercel Analytics？”">&#8203;</a></h2>
<p>在決定用哪套分析工具之前，我其實看了好幾個選項：</p>
<p><strong>Google Analytics</strong> 雖然功能強大，但 script 太肥、載入慢，而且要處理 Cookie 同意橫幅，感覺有點麻煩。</p>
<p><strong>Plausible / Fathom</strong> 這類隱私友善的分析服務都不錯，但要付費，而且要多維護一個服務。</p>
<p>最後選了 <strong>Vercel Analytics</strong> 的原因很簡單：</p>
<ol>
<li><strong>超輕量</strong>：script 只有約 1 KB，比 Google Analytics 小 45 倍</li>
<li><strong>不用 Cookie</strong>：完全符合 GDPR，不需要同意橫幅</li>
<li><strong>自動追蹤 Web Vitals</strong>：LCP、INP、CLS 這些效能指標都有，對 SEO 很重要</li>
<li><strong>無痛整合</strong>：部落格本來就在 Vercel 上，加幾行程式碼就搞定</li>
<li><strong>即時資料</strong>：不用等好幾個小時才看到數據</li>
</ol>
<hr>
<h2 id="vercel-analytics-vs-google-analytics" tabindex="-1">Vercel Analytics vs Google Analytics <a class="header-anchor" href="#vercel-analytics-vs-google-analytics" aria-label="Permalink to “Vercel Analytics vs Google Analytics”">&#8203;</a></h2>
<p>在選擇分析工具時，很多人第一個想到的都是 Google Analytics，但其實兩者的定位和使用情境蠻不一樣的。我整理了一個比較表：</p>
<table tabindex="0">
<thead>
<tr>
<th>項目</th>
<th>Vercel Analytics</th>
<th>Google Analytics (GA4)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Script 大小</strong></td>
<td>~1 KB</td>
<td>~45 KB</td>
</tr>
<tr>
<td><strong>效能影響</strong></td>
<td>極小</td>
<td>明顯</td>
</tr>
<tr>
<td><strong>Cookie</strong></td>
<td>不使用</td>
<td>使用</td>
</tr>
<tr>
<td><strong>GDPR 合規</strong></td>
<td>預設合規</td>
<td>需要同意橫幅</td>
</tr>
<tr>
<td><strong>設定難度</strong></td>
<td>超簡單（幾行程式碼）</td>
<td>複雜（需要設定追蹤 ID、事件等）</td>
</tr>
<tr>
<td><strong>Web Vitals</strong></td>
<td>自動追蹤（LCP、INP、CLS）</td>
<td>需要另外設定</td>
</tr>
<tr>
<td><strong>即時資料</strong></td>
<td>是</td>
<td>延遲數小時</td>
</tr>
<tr>
<td><strong>使用者行為分析</strong></td>
<td>基本</td>
<td>深入</td>
</tr>
<tr>
<td><strong>轉換追蹤</strong></td>
<td>有限</td>
<td>完整</td>
</tr>
<tr>
<td><strong>費用</strong></td>
<td>免費（50K events/月）<br>Pro: $10/月（100K events/月）</td>
<td>完全免費</td>
</tr>
<tr>
<td><strong>資料所有權</strong></td>
<td>Vercel</td>
<td>Google</td>
</tr>
</tbody>
</table>
<h3 id="我的使用建議" tabindex="-1">我的使用建議 <a class="header-anchor" href="#我的使用建議" aria-label="Permalink to “我的使用建議”">&#8203;</a></h3>
<p><strong>選 Vercel Analytics 如果你：</strong></p>
<ul>
<li>部落格已經在 Vercel 上</li>
<li>只想知道基本的流量和效能數據</li>
<li>在意網站載入速度</li>
<li>不想處理 Cookie 同意問題</li>
<li>想要即時看到資料</li>
</ul>
<p><strong>選 Google Analytics 如果你：</strong></p>
<ul>
<li>需要深入的使用者行為分析</li>
<li>要追蹤轉換和目標達成</li>
<li>需要自訂報表和細緻的區隔</li>
<li>要整合 Google Ads 等其他 Google 服務</li>
<li>不在意多一點效能負擔</li>
</ul>
<p><strong>我的作法：</strong></p>
<p>對個人部落格來說，Vercel Analytics 就很夠用了。它輕量、快速、資料也夠清楚。
如果真的需要更深入的分析，兩個一起用也可以，只是要記得處理 GA 的 Cookie 同意問題。</p>
<hr>
<h2 id="實作步驟" tabindex="-1">實作步驟 <a class="header-anchor" href="#實作步驟" aria-label="Permalink to “實作步驟”">&#8203;</a></h2>
<h3 id="第一步-安裝套件" tabindex="-1">第一步：安裝套件 <a class="header-anchor" href="#第一步-安裝套件" aria-label="Permalink to “第一步：安裝套件”">&#8203;</a></h3>
<p>首先要把 <code>@vercel/analytics</code> 套件裝起來。</p>
<p>我的專案用的是 pnpm：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> @vercel/analytics</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>如果你用 npm 或 yarn：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> @vercel/analytics</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># 或</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">yarn</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> @vercel/analytics</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><hr>
<h3 id="第二步-初始化-analytics" tabindex="-1">第二步：初始化 Analytics <a class="header-anchor" href="#第二步-初始化-analytics" aria-label="Permalink to “第二步：初始化 Analytics”">&#8203;</a></h3>
<p>接下來要在 VitePress 主題設定檔中初始化 Analytics。</p>
<p>打開 <code>docs/.vitepress/theme/index.ts</code>，加入以下程式碼：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> DefaultTheme </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vitepress/theme'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { Theme } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vitepress'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { inject } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@vercel/analytics'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './custom.css'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  extends: DefaultTheme,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  enhanceApp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">app</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 你原有的元件註冊...</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 初始化 Vercel Analytics（僅在客戶端執行）</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">typeof</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> window </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'undefined'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      inject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">} </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">satisfies</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Theme</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br></div></div><p>這段程式碼做了幾件重要的事：</p>
<ol>
<li><strong>客戶端檢查</strong>：<code>if (typeof window !== 'undefined')</code> 確保只在瀏覽器環境執行，避免 SSR 階段出錯</li>
<li><strong>TypeScript 類型</strong>：<code>import type { Theme }</code> 和 <code>satisfies Theme</code> 可以獲得完整的型別檢查</li>
<li><strong>初始化時機</strong>：在 <code>enhanceApp</code> 中初始化，確保應用程式啟動時就開始追蹤</li>
</ol>
<hr>
<h3 id="第三步-部署到-vercel" tabindex="-1">第三步：部署到 Vercel <a class="header-anchor" href="#第三步-部署到-vercel" aria-label="Permalink to “第三步：部署到 Vercel”">&#8203;</a></h3>
<p>把變更推到 GitHub，Vercel 就會自動觸發部署：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> .</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> commit</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -m</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "Add Vercel Analytics"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> push</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> origin</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> main</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>部署完成後，等個幾分鐘讓 Analytics 開始收集資料。</p>
<hr>
<h3 id="第四步-查看分析資料" tabindex="-1">第四步：查看分析資料 <a class="header-anchor" href="#第四步-查看分析資料" aria-label="Permalink to “第四步：查看分析資料”">&#8203;</a></h3>
<ol>
<li>登入 <a href="https://vercel.com" target="_blank" rel="noreferrer">Vercel Dashboard</a></li>
<li>選擇你的專案</li>
<li>點擊上方的 <strong>Analytics</strong> 分頁</li>
</ol>
<p>你會看到以下資料：</p>
<ul>
<li><strong>頁面瀏覽量</strong>：哪些頁面最受歡迎</li>
<li><strong>訪客數量</strong>：不重複訪客統計</li>
<li><strong>地理位置</strong>：訪客來自哪些國家/地區</li>
<li><strong>裝置類型</strong>：桌面、平板、手機的比例</li>
<li><strong>Web Vitals</strong>：LCP（最大內容繪製）、INP（互動到下次繪製）、CLS（累積版面配置位移）等效能指標</li>
</ul>
<hr>
<h2 id="進階用法-追蹤自訂事件" tabindex="-1">進階用法：追蹤自訂事件 <a class="header-anchor" href="#進階用法-追蹤自訂事件" aria-label="Permalink to “進階用法：追蹤自訂事件”">&#8203;</a></h2>
<p>基本的頁面瀏覽追蹤設定好之後，如果想追蹤更具體的使用者行為（例如：點按鈕、送出表單、留言等），可以用 <code>track</code> 函式。</p>
<h3 id="範例-追蹤留言提交" tabindex="-1">範例：追蹤留言提交 <a class="header-anchor" href="#範例-追蹤留言提交" aria-label="Permalink to “範例：追蹤留言提交”">&#8203;</a></h3>
<p>假設你的部落格有 Waline 留言系統，想知道有多少人提交留言：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { track } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@vercel/analytics'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 在留言提交成功後呼叫</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">track</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'comment_submitted'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  article: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'文章標題或 URL'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  category: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'notes'</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> // 或 'learn', 'misc'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><h3 id="範例-追蹤外部連結點擊" tabindex="-1">範例：追蹤外部連結點擊 <a class="header-anchor" href="#範例-追蹤外部連結點擊" aria-label="Permalink to “範例：追蹤外部連結點擊”">&#8203;</a></h3>
<p>如果想知道有多少人點了文章中的外部連結：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ts"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { track } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@vercel/analytics'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> trackExternalLink</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">url</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  track</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'external_link_click'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { url })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://example.com"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    @click</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"trackExternalLink('https://example.com')"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    target</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"_blank"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    外部連結</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><h3 id="在-vercel-dashboard-查看自訂事件" tabindex="-1">在 Vercel Dashboard 查看自訂事件 <a class="header-anchor" href="#在-vercel-dashboard-查看自訂事件" aria-label="Permalink to “在 Vercel Dashboard 查看自訂事件”">&#8203;</a></h3>
<p>自訂事件會出現在 Analytics → Events 分頁，可以看到：</p>
<ul>
<li>事件發生次數</li>
<li>事件參數（例如：文章名稱、URL 等）</li>
<li>時間序列圖表</li>
</ul>
<hr>
<h2 id="開發環境注意事項" tabindex="-1">開發環境注意事項 <a class="header-anchor" href="#開發環境注意事項" aria-label="Permalink to “開發環境注意事項”">&#8203;</a></h2>
<p>在本地開發時（<code>pnpm dev</code>），Analytics <strong>不會</strong>送資料，這是預期的行為：</p>
<ul>
<li>避免開發時的測試資料污染正式統計</li>
<li>不會影響開發體驗</li>
<li>只在 production build 生效</li>
</ul>
<p>如果想在本地測試 Analytics，可以用 production 模式預覽：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> build</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> serve</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><hr>
<h2 id="效能影響" tabindex="-1">效能影響 <a class="header-anchor" href="#效能影響" aria-label="Permalink to “效能影響”">&#8203;</a></h2>
<p>Vercel Analytics 的 script 真的超輕量：</p>
<ul>
<li><strong>Script 大小</strong>：約 1 KB，比 Google Analytics（約 45 KB）小 45 倍</li>
<li><strong>載入方式</strong>：非同步載入，不會卡住頁面渲染</li>
<li><strong>執行時機</strong>：在頁面 load 事件後才執行</li>
<li><strong>網路請求</strong>：只在必要時才送資料</li>
</ul>
<p>官方數據顯示，Vercel Analytics 是 44x smaller than Google Analytics，對網站效能的影響真的小很多。</p>
<hr>
<h2 id="隱私與合規" tabindex="-1">隱私與合規 <a class="header-anchor" href="#隱私與合規" aria-label="Permalink to “隱私與合規”">&#8203;</a></h2>
<p>Vercel Analytics 在隱私方面做得不錯：</p>
<ol>
<li><strong>不用 Cookie</strong>：完全符合 GDPR 規範</li>
<li><strong>匿名追蹤</strong>：不收集個人識別資訊</li>
<li><strong>無第三方追蹤器</strong>：資料只在 Vercel 內部處理</li>
<li><strong>透明的資料政策</strong>：<a href="https://vercel.com/legal/privacy-policy" target="_blank" rel="noreferrer">Vercel Privacy Policy</a></li>
</ol>
<p>這表示你不用在網站上放煩人的 Cookie 同意橫幅，使用者體驗好很多。</p>
<hr>
<h2 id="常見問題" tabindex="-1">常見問題 <a class="header-anchor" href="#常見問題" aria-label="Permalink to “常見問題”">&#8203;</a></h2>
<h3 id="q-免費版有限制嗎" tabindex="-1">Q: 免費版有限制嗎？ <a class="header-anchor" href="#q-免費版有限制嗎" aria-label="Permalink to “Q: 免費版有限制嗎？”">&#8203;</a></h3>
<p>Vercel Analytics 的免費版（Hobby Plan）包含：</p>
<ul>
<li><strong>每月 50,000 events</strong>（2025 年更新，之前是 2.5K）</li>
<li>Events 包含頁面瀏覽、Web Vitals 等所有追蹤事件</li>
<li>基本的流量統計和 Web Vitals 監控</li>
</ul>
<p><strong>重要限制：</strong></p>
<ul>
<li>所有專案共用額度：同一個 Vercel 帳號下的所有專案會累加計算</li>
<li>超過後會暫停收集：超過限制有 3 天寬限期，之後停止收集資料</li>
<li>要等 7 天才會重新開始，或者升級到 Pro Plan</li>
<li>僅限個人非商業用途</li>
</ul>
<p><strong>Pro Plan 的差異：</strong></p>
<ul>
<li>每月 100,000 events</li>
<li>超過後可以付費繼續使用</li>
<li>可用於商業專案</li>
</ul>
<p>對一般個人部落格來說，50K events/月應該很夠用了。真的超過的話再考慮升級。</p>
<h3 id="q-什麼是-events-跟頁面瀏覽有什麼不同" tabindex="-1">Q: 什麼是 Events？跟頁面瀏覽有什麼不同？ <a class="header-anchor" href="#q-什麼是-events-跟頁面瀏覽有什麼不同" aria-label="Permalink to “Q: 什麼是 Events？跟頁面瀏覽有什麼不同？”">&#8203;</a></h3>
<p><strong>Events</strong> 是 Vercel Analytics 計算用量的單位，包含所有追蹤的資料點：</p>
<ul>
<li>頁面瀏覽（Page Views）</li>
<li>Web Vitals 數據（LCP、INP、CLS 等）</li>
<li>自訂事件（如果你有用 <code>track()</code> 函式）</li>
</ul>
<p><strong>舉例來說：</strong>
一個使用者造訪你的部落格文章，可能會產生：</p>
<ul>
<li>1 個頁面瀏覽 event</li>
<li>3 個 Web Vitals events（LCP、INP、CLS 各 1 個）</li>
<li>總共 4 個 events</li>
</ul>
<p>所以 50K events/月 並不等於 50K 頁面瀏覽，實際上大約是 <strong>12,500 次頁面瀏覽</strong>左右（假設每次瀏覽產生 4 個 events）。</p>
<h3 id="q-inp-是什麼-為什麼不是-fid" tabindex="-1">Q: INP 是什麼？為什麼不是 FID？ <a class="header-anchor" href="#q-inp-是什麼-為什麼不是-fid" aria-label="Permalink to “Q: INP 是什麼？為什麼不是 FID？”">&#8203;</a></h3>
<p><strong>INP（Interaction to Next Paint）</strong> 是 Google 在 2024 年 3 月推出的新 Core Web Vital 指標，用來取代舊的 FID（First Input Delay）。</p>
<p><strong>主要差異：</strong></p>
<ul>
<li><strong>FID</strong>：只測量「第一次」使用者互動的延遲</li>
<li><strong>INP</strong>：測量頁面上「所有」互動的回應速度，更能反映真實的使用體驗</li>
</ul>
<p>INP 會追蹤所有的點擊、按鍵、輸入等互動，並記錄從互動到畫面更新的時間。這對 SEO 很重要，因為 Google 搜尋排名會參考這個指標。</p>
<h3 id="q-可以和-google-analytics-一起用嗎" tabindex="-1">Q: 可以和 Google Analytics 一起用嗎？ <a class="header-anchor" href="#q-可以和-google-analytics-一起用嗎" aria-label="Permalink to “Q: 可以和 Google Analytics 一起用嗎？”">&#8203;</a></h3>
<p>可以！兩個不衝突，你可以同時用：</p>
<ul>
<li>Vercel Analytics 看即時資料和 Web Vitals</li>
<li>Google Analytics 做更深入的使用者行為分析</li>
</ul>
<p>但老實說，對一般部落格來說，Vercel Analytics 就夠用了。</p>
<h3 id="q-資料可以匯出嗎" tabindex="-1">Q: 資料可以匯出嗎？ <a class="header-anchor" href="#q-資料可以匯出嗎" aria-label="Permalink to “Q: 資料可以匯出嗎？”">&#8203;</a></h3>
<p>Pro Plan 以上可以透過 Vercel API 匯出原始資料，但 Hobby Plan 只能在 Dashboard 看。</p>
<hr>
<h2 id="總結" tabindex="-1">總結 <a class="header-anchor" href="#總結" aria-label="Permalink to “總結”">&#8203;</a></h2>
<p>整合 Vercel Analytics 到 VitePress 其實超簡單，主要就是：</p>
<ol>
<li>裝套件</li>
<li>加幾行程式碼初始化</li>
<li>推到 Vercel 部署</li>
<li>開始收集資料</li>
</ol>
<p>用起來的感覺很不錯，輕量、快速、不用處理 Cookie 問題，還能追蹤 Web Vitals。
如果你的部落格本來就在 Vercel 上，真的沒理由不用！</p>
<p>包括你正在看的這個部落格，也是用 Vercel Analytics 在追蹤流量和效能指標。</p>
<hr>
<h2 id="參考資源" tabindex="-1">參考資源 <a class="header-anchor" href="#參考資源" aria-label="Permalink to “參考資源”">&#8203;</a></h2>
<ul>
<li><a href="https://vercel.com/docs/analytics" target="_blank" rel="noreferrer">Vercel Analytics 官方文件</a></li>
<li><a href="https://www.npmjs.com/package/@vercel/analytics" target="_blank" rel="noreferrer">@vercel/analytics NPM 套件</a></li>
<li><a href="https://vitepress.dev/" target="_blank" rel="noreferrer">VitePress 官方文件</a></li>
</ul>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vitepress</category>
            <category>vercel</category>
            <category>analytics</category>
        </item>
        <item>
            <title><![CDATA[為 VitePress 部落格加上 RSS Feed 訂閱功能]]></title>
            <link>https://kurohsu.dev/notes/adding-rss-feed-to-vitepress.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/adding-rss-feed-to-vitepress.html</guid>
            <pubDate>Fri, 07 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="為-vitepress-部落格加上-rss-feed-訂閱功能" tabindex="-1">為 VitePress 部落格加上 RSS Feed 訂閱功能 <a class="header-anchor" href="#為-vitepress-部落格加上-rss-feed-訂閱功能" aria-label="Permalink to “為 VitePress 部落格加上 RSS Feed 訂閱功能”">&#8203;</a></h1>
<p>晚上花了一點時間實作日語文章的切換，剛好 Cash 大就在 X (Twitter) 上敲碗問我什麼時候要加 RSS Feed XD:</p>
<p><img src="/images/notes/rss-feed-request.png" alt="Cash 大敲碗 RSS Feed"></p>
<p>想想 RSS Reader 還是追蹤部落格最方便的方式，不用每天手動檢查有沒有新文章，於是決定著手加上這個功能。
但 VitePress 本身沒有內建 RSS 功能，但透過 <code>buildEnd</code> hook 和 <code>feed</code> 這個 npm 套件，其實實作起來並不複雜。</p>
<p>這篇文章就來記錄一下整個實作過程。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="為-vitepress-部落格加上-rss-feed-訂閱功能" tabindex="-1">為 VitePress 部落格加上 RSS Feed 訂閱功能 <a class="header-anchor" href="#為-vitepress-部落格加上-rss-feed-訂閱功能" aria-label="Permalink to “為 VitePress 部落格加上 RSS Feed 訂閱功能”">&#8203;</a></h1>
<p>晚上花了一點時間實作日語文章的切換，剛好 Cash 大就在 X (Twitter) 上敲碗問我什麼時候要加 RSS Feed XD:</p>
<p><img src="/images/notes/rss-feed-request.png" alt="Cash 大敲碗 RSS Feed"></p>
<p>想想 RSS Reader 還是追蹤部落格最方便的方式，不用每天手動檢查有沒有新文章，於是決定著手加上這個功能。
但 VitePress 本身沒有內建 RSS 功能，但透過 <code>buildEnd</code> hook 和 <code>feed</code> 這個 npm 套件，其實實作起來並不複雜。</p>
<p>這篇文章就來記錄一下整個實作過程。</p>
<hr>
<h2 id="為什麼要提供-rss-feed" tabindex="-1">為什麼要提供 RSS Feed？ <a class="header-anchor" href="#為什麼要提供-rss-feed" aria-label="Permalink to “為什麼要提供 RSS Feed？”">&#8203;</a></h2>
<p>在社群媒體主導的年代，RSS 看起來有點過時，但其實還是有不少優點：</p>
<ol>
<li><strong>無演算法干擾</strong>：RSS 閱讀器會照時間順序顯示所有文章，不會被演算法過濾</li>
<li><strong>隱私友善</strong>：不需要註冊帳號，不會被追蹤</li>
<li><strong>跨平台</strong>：可以在各種裝置、各種 RSS 閱讀器中訂閱</li>
<li><strong>離線閱讀</strong>：很多 RSS 閱讀器支援離線下載</li>
<li><strong>方便整合</strong>：RSS 是標準格式，可以串接到各種自動化工具</li>
</ol>
<p>對於技術部落格來說，讀者群中用 RSS 的比例還蠻高的，所以這個功能還是很值得做。</p>
<hr>
<h2 id="實作架構" tabindex="-1">實作架構 <a class="header-anchor" href="#實作架構" aria-label="Permalink to “實作架構”">&#8203;</a></h2>
<p>整個 RSS 生成系統分成兩個部分：</p>
<ol>
<li>
<p><strong>生成腳本</strong> (<code>scripts/generateRssFeed.ts</code>)：</p>
<ul>
<li>載入所有文章資料</li>
<li>建立 RSS Feed 物件</li>
<li>生成三種格式的 feed 檔案</li>
</ul>
</li>
<li>
<p><strong>VitePress 整合</strong> (<code>docs/.vitepress/config.ts</code>)：</p>
<ul>
<li>在 <code>buildEnd</code> hook 呼叫生成腳本</li>
<li>確保每次建置都會產生最新的 feed</li>
</ul>
</li>
</ol>
<p>技術選擇：</p>
<ul>
<li><strong>feed</strong> 套件：業界標準的 RSS feed 生成工具</li>
<li><strong>VitePress <code>createContentLoader</code></strong>：複用現有的文章載入機制</li>
<li><strong>buildEnd hook</strong>：在建置完成後自動執行</li>
</ul>
<hr>
<h2 id="第一步-安裝-feed-套件" tabindex="-1">第一步：安裝 feed 套件 <a class="header-anchor" href="#第一步-安裝-feed-套件" aria-label="Permalink to “第一步：安裝 feed 套件”">&#8203;</a></h2>
<p>首先要安裝 <code>feed</code> 這個 npm 套件，它可以幫我們產生符合標準的 RSS、Atom 和 JSON Feed。</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -D</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> feed</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>如果你用 npm 或 yarn：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --save-dev</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> feed</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># or</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">yarn</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -D</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> feed</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><hr>
<h2 id="第二步-建立-rss-生成腳本" tabindex="-1">第二步：建立 RSS 生成腳本 <a class="header-anchor" href="#第二步-建立-rss-生成腳本" aria-label="Permalink to “第二步：建立 RSS 生成腳本”">&#8203;</a></h2>
<p>在專案中建立 <code>scripts/generateRssFeed.ts</code>：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { Feed } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'feed'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { createContentLoader, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> SiteConfig } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vitepress'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'path'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { writeFileSync } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'fs'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> siteUrl</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'https://kurohsu.dev'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> siteTitle</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'Kuro Hsu 的筆記'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> siteDescription</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '技術筆記、學習紀錄與生活記錄'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> generateRssFeed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">config</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> SiteConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 建立 Feed 物件</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> feed</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Feed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    title: siteTitle,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    description: siteDescription,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    id: siteUrl,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    link: siteUrl,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    language: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'zh-TW'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    favicon: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/favicon.ico`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    copyright: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Copyright © 2025 Kuro Hsu'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    feedLinks: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      rss: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/feed.xml`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      atom: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/atom.xml`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      json: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/feed.json`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    author: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Kuro Hsu'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      link: siteUrl,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 載入所有文章（包含 excerpt 和 rendered HTML）</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createContentLoader</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    'notes/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    'learn/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    'misc/**/*.md'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ], {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    excerpt: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 載入摘要</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    render: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 產生完整 HTML</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    transform</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">rawData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> rawData</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">url</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">url.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">endsWith</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/index.html'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 排除索引頁</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">url</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">url.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.ja.html'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">))    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 排除日文文章</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">frontmatter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          frontmatter.title </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> frontmatter.date           </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 必須有標題和日期</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        )</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">frontmatter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">frontmatter.draft)  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 排除草稿</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">frontmatter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          frontmatter.lang </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'ja'</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">                       // 再次確認不是日文</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        )</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">url</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">frontmatter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">excerpt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">          // 將 URL 轉換為絕對路徑</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> rewrittenUrl</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> url.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            /</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">(notes</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">|</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">learn</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">|</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">misc)</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\d</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">{4}</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\d</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">{2}</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">html)</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            '/$1/$2'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          )</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">          return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            title: frontmatter.title,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            url: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">rewrittenUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            date: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(frontmatter.date),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            description: excerpt </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> frontmatter.description </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            content: html </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            author: frontmatter.author </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'Kuro Hsu'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            category: frontmatter.tags?.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">tag</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ({ name: tag })) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sort</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">b</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> b.date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a.date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()) </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 按日期降冪排序</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">load</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 將文章加入 feed</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> post</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> of</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> posts) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    feed.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">addItem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      title: post.title,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      id: post.url,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      link: post.url,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      description: post.description,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      content: post.content,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      author: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          name: post.author,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          link: siteUrl,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      date: post.date,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      category: post.category,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 寫入三種格式的 feed 檔案</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> outDir</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> config.outDir</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  writeFileSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outDir, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'feed.xml'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), feed.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">rss2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">())</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  writeFileSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outDir, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'atom.xml'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), feed.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">atom1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">())</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  writeFileSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outDir, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'feed.json'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), feed.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">json1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">())</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`✅ RSS feeds generated with ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">posts</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">} posts:`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`   - ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/feed.xml (RSS 2.0)`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`   - ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/atom.xml (Atom 1.0)`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`   - ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/feed.json (JSON Feed 1.0)`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br><span class="line-number">55</span><br><span class="line-number">56</span><br><span class="line-number">57</span><br><span class="line-number">58</span><br><span class="line-number">59</span><br><span class="line-number">60</span><br><span class="line-number">61</span><br><span class="line-number">62</span><br><span class="line-number">63</span><br><span class="line-number">64</span><br><span class="line-number">65</span><br><span class="line-number">66</span><br><span class="line-number">67</span><br><span class="line-number">68</span><br><span class="line-number">69</span><br><span class="line-number">70</span><br><span class="line-number">71</span><br><span class="line-number">72</span><br><span class="line-number">73</span><br><span class="line-number">74</span><br><span class="line-number">75</span><br><span class="line-number">76</span><br><span class="line-number">77</span><br><span class="line-number">78</span><br><span class="line-number">79</span><br><span class="line-number">80</span><br><span class="line-number">81</span><br><span class="line-number">82</span><br><span class="line-number">83</span><br><span class="line-number">84</span><br><span class="line-number">85</span><br><span class="line-number">86</span><br><span class="line-number">87</span><br><span class="line-number">88</span><br><span class="line-number">89</span><br><span class="line-number">90</span><br><span class="line-number">91</span><br><span class="line-number">92</span><br><span class="line-number">93</span><br><span class="line-number">94</span><br><span class="line-number">95</span><br><span class="line-number">96</span><br><span class="line-number">97</span><br><span class="line-number">98</span><br><span class="line-number">99</span><br><span class="line-number">100</span><br></div></div><p>這個腳本做了幾件重要的事：</p>
<ol>
<li><strong>建立 Feed 物件</strong>：設定網站基本資訊和元資料</li>
<li><strong>載入文章資料</strong>：使用 VitePress 的 <code>createContentLoader</code> API</li>
<li><strong>過濾和轉換</strong>：排除不需要的頁面，轉換 URL 格式</li>
<li><strong>產生三種格式</strong>：RSS 2.0、Atom 1.0、JSON Feed 1.0</li>
</ol>
<hr>
<h2 id="第三步-整合到-vitepress-建置流程" tabindex="-1">第三步：整合到 VitePress 建置流程 <a class="header-anchor" href="#第三步-整合到-vitepress-建置流程" aria-label="Permalink to “第三步：整合到 VitePress 建置流程”">&#8203;</a></h2>
<p>在 <code>docs/.vitepress/config.ts</code> 加入 <code>buildEnd</code> hook：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { defineConfig } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vitepress'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { generateRssFeed } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '../../scripts/generateRssFeed'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 建置完成後生成 RSS feed</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> buildEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">siteConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> generateRssFeed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(siteConfig)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ... 其他設定</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><p>就這麼簡單！每次執行 <code>pnpm build</code> 時，VitePress 會在建置完成後自動呼叫 <code>generateRssFeed</code>，產生最新的 RSS feed。</p>
<hr>
<h2 id="第四步-測試" tabindex="-1">第四步：測試 <a class="header-anchor" href="#第四步-測試" aria-label="Permalink to “第四步：測試”">&#8203;</a></h2>
<p>執行建置命令：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> build</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>如果一切順利，你會在 console 看到：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>✅ RSS feeds generated with 46 posts:</span></span>
<span class="line"><span>   - https://kurohsu.dev/feed.xml (RSS 2.0)</span></span>
<span class="line"><span>   - https://kurohsu.dev/atom.xml (Atom 1.0)</span></span>
<span class="line"><span>   - https://kurohsu.dev/feed.json (JSON Feed 1.0)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p>檢查 <code>docs/.vitepress/dist</code> 目錄，應該會看到三個檔案：</p>
<ul>
<li><code>feed.xml</code> - RSS 2.0 格式</li>
<li><code>atom.xml</code> - Atom 1.0 格式</li>
<li><code>feed.json</code> - JSON Feed 1.0 格式</li>
</ul>
<p>你可以用 RSS 閱讀器測試這些 feed，確認內容正確。</p>
<hr>
<h2 id="為什麼要產生三種格式" tabindex="-1">為什麼要產生三種格式？ <a class="header-anchor" href="#為什麼要產生三種格式" aria-label="Permalink to “為什麼要產生三種格式？”">&#8203;</a></h2>
<p>你可能會好奇，為什麼要同時產生 RSS、Atom 和 JSON Feed 三種格式？</p>
<ol>
<li>
<p><strong>RSS 2.0</strong>：</p>
<ul>
<li>最古老也最廣泛支援的格式</li>
<li>幾乎所有 RSS 閱讀器都支援</li>
<li>檔案較小，適合傳輸</li>
</ul>
</li>
<li>
<p><strong>Atom 1.0</strong>：</p>
<ul>
<li>較新的標準，規範更嚴謹</li>
<li>支援更多元資料（例如作者資訊）</li>
<li>某些現代工具偏好使用 Atom</li>
</ul>
</li>
<li>
<p><strong>JSON Feed</strong>：</p>
<ul>
<li>最新的格式，使用 JSON 而非 XML</li>
<li>對開發者更友善，容易解析</li>
<li>適合用於 API 整合</li>
</ul>
</li>
</ol>
<p><code>feed</code> 套件可以同時產生三種格式，幾乎沒有額外成本，所以就全部提供了。
這樣無論讀者用什麼工具，都能找到適合的格式。</p>
<hr>
<h2 id="關於文章過濾" tabindex="-1">關於文章過濾 <a class="header-anchor" href="#關於文章過濾" aria-label="Permalink to “關於文章過濾”">&#8203;</a></h2>
<p>在我的實作中，我刻意排除了日文文章：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">url</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">url.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.ja.html'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">))    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 排除日文文章</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">frontmatter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> frontmatter.lang </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'ja'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p>這是因為：</p>
<ol>
<li><strong>目標讀者</strong>：RSS 訂閱者主要是繁體中文讀者</li>
<li><strong>避免重複</strong>：有些文章同時有中文和日文版本，不希望在 feed 中出現兩次</li>
<li><strong>保持簡潔</strong>：Feed 只包含中文內容，訂閱者不會收到看不懂的文章</li>
</ol>
<p>如果你的 VitePress 網站是單一語言，這段可以跳過當作沒看到，因為這是我自己的需求。</p>
<hr>
<h2 id="實用技巧-url-rewriting" tabindex="-1">實用技巧：URL Rewriting <a class="header-anchor" href="#實用技巧-url-rewriting" aria-label="Permalink to “實用技巧：URL Rewriting”">&#8203;</a></h2>
<p>我的部落格文章檔案是按年月組織的（<code>notes/2025/11/article.md</code>），但對外的 URL 是扁平的（<code>/notes/article.html</code>）。</p>
<p>這是透過 VitePress 的 rewrites 功能實現的，但在 RSS feed 中也需要做同樣的轉換：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> rewrittenUrl</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> url.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  /</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">(notes</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">|</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">learn</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">|</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">misc)</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\d</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">{4}</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\d</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">{2}</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">html)</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  '/$1/$2'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p>這個正則表達式會：</p>
<ul>
<li>匹配 <code>/notes/2025/11/article.html</code> 這種格式</li>
<li>轉換成 <code>/notes/article.html</code></li>
<li>確保 RSS feed 中的連結跟實際 URL 一致</li>
</ul>
<p>如果你的網站沒有用 rewrites，可以直接使用原始的 <code>url</code>。</p>
<hr>
<h2 id="踩過的坑" tabindex="-1">踩過的坑 <a class="header-anchor" href="#踩過的坑" aria-label="Permalink to “踩過的坑”">&#8203;</a></h2>
<h3 id="_1-createcontentloader-的路徑問題" tabindex="-1">1. createContentLoader 的路徑問題 <a class="header-anchor" href="#_1-createcontentloader-的路徑問題" aria-label="Permalink to “1. createContentLoader 的路徑問題”">&#8203;</a></h3>
<p>一開始我寫成：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createContentLoader</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'docs/notes/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 錯誤</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'docs/learn/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'docs/misc/**/*.md'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">])</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>結果載入不到任何文章。後來發現 <code>createContentLoader</code> 的路徑是相對於 <code>docs</code> 目錄，應該寫成：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createContentLoader</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'notes/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 正確</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'learn/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'misc/**/*.md'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">])</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><h3 id="_2-忘記設定-excerpt-和-render" tabindex="-1">2. 忘記設定 excerpt 和 render <a class="header-anchor" href="#_2-忘記設定-excerpt-和-render" aria-label="Permalink to “2. 忘記設定 excerpt 和 render”">&#8203;</a></h3>
<p>如果沒有設定這兩個選項：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  excerpt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 取得摘要</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  render</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 產生完整 HTML</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p>feed 中就不會有文章內容，只有標題和連結，體驗很差。</p>
<h3 id="_3-建置時間變長" tabindex="-1">3. 建置時間變長 <a class="header-anchor" href="#_3-建置時間變長" aria-label="Permalink to “3. 建置時間變長”">&#8203;</a></h3>
<p>加入 RSS 生成後，建置時間會增加幾秒（我的部落格約增加 2-3 秒）。</p>
<p>這是正常的，因為需要：</p>
<ul>
<li>載入所有文章資料</li>
<li>渲染完整的 HTML</li>
<li>產生三個 feed 檔案</li>
</ul>
<p>如果建置時間成為問題，可以考慮：</p>
<ul>
<li>只產生一種格式（例如只產生 RSS 2.0）</li>
<li>限制 feed 中的文章數量（例如只包含最新 20 篇）</li>
<li>不要 render 完整 HTML，只提供摘要</li>
</ul>
<hr>
<h2 id="進階-只顯示最新文章" tabindex="-1">進階：只顯示最新文章 <a class="header-anchor" href="#進階-只顯示最新文章" aria-label="Permalink to “進階：只顯示最新文章”">&#8203;</a></h2>
<p>如果你的部落格文章很多，可以考慮只在 feed 中顯示最新的幾篇：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sort</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">b</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> b.date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a.date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getTime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">())</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">slice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 只取前 20 篇</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p>這樣可以：</p>
<ul>
<li>減少 feed 檔案大小</li>
<li>加快建置速度</li>
<li>降低 RSS 閱讀器的負擔</li>
</ul>
<p>不過我個人選擇包含所有文章，因為有些讀者可能想從頭開始閱讀，而且我的文章數量還不算太多。</p>
<hr>
<h2 id="讓讀者知道有-rss" tabindex="-1">讓讀者知道有 RSS <a class="header-anchor" href="#讓讀者知道有-rss" aria-label="Permalink to “讓讀者知道有 RSS”">&#8203;</a></h2>
<p>產生 RSS feed 後，別忘了讓讀者知道可以訂閱！</p>
<p>可以在網站上加個訂閱連結，例如在導覽列或頁尾：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/feed.xml"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">svg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- RSS icon --></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">svg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  訂閱 RSS</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p>或者在 <code>&lt;head&gt;</code> 加入 RSS 自動探索標籤，讓瀏覽器和 RSS 閱讀器可以自動找到 feed：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">link</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> rel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"alternate"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"application/rss+xml"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Kuro Hsu 的筆記"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://kurohsu.dev/feed.xml"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>VitePress 可以透過 <code>head</code> 設定來加入：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  head: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'link'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      rel: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'alternate'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      type: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'application/rss+xml'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      title: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Kuro Hsu 的筆記'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      href: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://kurohsu.dev/feed.xml'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><hr>
<h2 id="總結" tabindex="-1">總結 <a class="header-anchor" href="#總結" aria-label="Permalink to “總結”">&#8203;</a></h2>
<p>為 VitePress 加上 RSS feed 功能其實很簡單，主要步驟就是：</p>
<ol>
<li>安裝 <code>feed</code> 套件</li>
<li>建立 RSS 生成腳本</li>
<li>在 <code>buildEnd</code> hook 呼叫腳本</li>
<li>測試並部署</li>
</ol>
<p>整個實作大約花了一小時左右，包含測試和調整。</p>
<p>使用 VitePress 的 <code>createContentLoader</code> API 讓事情變得更簡單，不需要自己去掃描和解析 markdown 檔案，也不需要擔心 frontmatter 格式。
而且因為是在打包建置時產生，所以不會影響網站的執行效能。</p>
<p>雖然裡面有超多客製化的部分是因應我自己的需求，但整體架構是通用的，
實作時可以根據自己的情況做調整。</p>
<p>如果你也在用 VitePress 架部落格，非常推薦加上這個功能。
雖然如今 RSS 已經不算是主流，但還是最方便的追蹤方式。</p>
<hr>
<p><strong>相關閱讀：</strong></p>
<ul>
<li><a href="/notes/integrating-waline-comments-to-vitepress.html">為 VitePress 部落格加上 Waline 留言系統</a></li>
<li><a href="/notes/organizing-vitepress-articles-by-date.html">用 VitePress Rewrites 按日期整理原始 markdown</a></li>
<li><a href="/notes/vitepress-自動生成-og-圖片.html">VitePress 自動生成 Open Graph 圖片</a></li>
</ul>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vitepress</category>
            <category>rss</category>
            <category>feed</category>
        </item>
        <item>
            <title><![CDATA[JavaScript 設計模式筆記 - SOLID 原則與 Vue.js 應用]]></title>
            <link>https://kurohsu.dev/notes/javascript-design-patterns-study-notes.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/javascript-design-patterns-study-notes.html</guid>
            <pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="javascript-設計模式筆記-solid-原則與-vue-js-應用" tabindex="-1">JavaScript 設計模式筆記 - SOLID 原則與 Vue.js 應用 <a class="header-anchor" href="#javascript-設計模式筆記-solid-原則與-vue-js-應用" aria-label="Permalink to “JavaScript 設計模式筆記 - SOLID 原則與 Vue.js 應用”">&#8203;</a></h1>
<p>2024 年初參加了一場 JavaScript 設計模式的讀書會，主題是討論 Addy Osmani 的《<a href="https://www.tenlong.com.tw/products/9786263247123" target="_blank" rel="noreferrer">JavaScript 設計模式學習手冊</a>》。
最近在重建部落格時，想起這場讀書會還沒留下點什麼紀錄有些可惜，
而且這種主題沒有時效問題，就決定趁這個機會整理一下當時的內容，順便 <s>水一篇</s> 分享給大家。</p>
<p>我負責的是最初的導讀，所以說雖然 Addy Osmani 書中是以 React 為例，但由於我的私心 (笑) 就盡量用我最熟悉的 Vue.js 的觀點來說明設計模式的概念與應用。</p>
<p>這篇文章整理了當時讀書會的重點內容，包含設計模式的基本概念、SOLID 原則，以及一些常見設計模式在 Vue.js 中的應用。</p>
<p>對於開發者來說，設計模式一直是個既熟悉又陌生的主題，說熟悉，是因為我們可能每天都在用；陌生，是因為用的時候你可能不一定知道自己正在用。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="javascript-設計模式筆記-solid-原則與-vue-js-應用" tabindex="-1">JavaScript 設計模式筆記 - SOLID 原則與 Vue.js 應用 <a class="header-anchor" href="#javascript-設計模式筆記-solid-原則與-vue-js-應用" aria-label="Permalink to “JavaScript 設計模式筆記 - SOLID 原則與 Vue.js 應用”">&#8203;</a></h1>
<p>2024 年初參加了一場 JavaScript 設計模式的讀書會，主題是討論 Addy Osmani 的《<a href="https://www.tenlong.com.tw/products/9786263247123" target="_blank" rel="noreferrer">JavaScript 設計模式學習手冊</a>》。
最近在重建部落格時，想起這場讀書會還沒留下點什麼紀錄有些可惜，
而且這種主題沒有時效問題，就決定趁這個機會整理一下當時的內容，順便 <s>水一篇</s> 分享給大家。</p>
<p>我負責的是最初的導讀，所以說雖然 Addy Osmani 書中是以 React 為例，但由於我的私心 (笑) 就盡量用我最熟悉的 Vue.js 的觀點來說明設計模式的概念與應用。</p>
<p>這篇文章整理了當時讀書會的重點內容，包含設計模式的基本概念、SOLID 原則，以及一些常見設計模式在 Vue.js 中的應用。</p>
<p>對於開發者來說，設計模式一直是個既熟悉又陌生的主題，說熟悉，是因為我們可能每天都在用；陌生，是因為用的時候你可能不一定知道自己正在用。</p>
<hr>
<h2 id="為什麼要學設計模式" tabindex="-1">為什麼要學設計模式？ <a class="header-anchor" href="#為什麼要學設計模式" aria-label="Permalink to “為什麼要學設計模式？”">&#8203;</a></h2>
<blockquote>
<p>Good code is like a love letter to the next developer who will maintain it!
好的程式碼就像寫給下一個維護它的開發人員的情書！
— Addy Osmani</p>
</blockquote>
<p>這句話真的很經典。每次接手別人的專案，都希望看到的是「祖產豪宅」，結果往往是「危樓廢墟」。
設計模式就是幫助我們寫出好維護、好擴充程式碼的工具。</p>
<p><strong>設計模式的好處：</strong></p>
<ul>
<li>容易增減功能</li>
<li>容易維護</li>
<li>容易重複使用程式碼</li>
<li>團隊合作時更容易溝通與分工</li>
</ul>
<p><strong>但也要小心過度使用：</strong></p>
<ul>
<li>過度複雜，讓其他開發者難以理解</li>
<li>過度設計（Over-engineering）</li>
<li>選擇了不適合的設計模式</li>
</ul>
<hr>
<h2 id="設計模式是什麼" tabindex="-1">設計模式是什麼？ <a class="header-anchor" href="#設計模式是什麼" aria-label="Permalink to “設計模式是什麼？”">&#8203;</a></h2>
<p>根據 Wikipedia 的定義：</p>
<blockquote>
<p>在軟體工程中，軟體設計模式是在軟體設計的給定上下文中，針對普遍問題的一種通用且可重用的解決方案。</p>
</blockquote>
<p>簡單來說：<strong>「有些人已經解決你的問題了！」</strong></p>
<p>設計模式是一套解決程式設計問題的通用解決方案集合。它們：</p>
<ul>
<li>是一種抽象的模板，可以用於不同的情況和程式語言</li>
<li>跟演算法有點像，但更偏向解決架構層面的問題</li>
<li>一再重複出現的東西、事件、現象，就稱為「模式」</li>
<li>你也可以建立自己的設計模式，只要它能解決問題並且能夠被重複使用</li>
</ul>
<h3 id="經典的-gof" tabindex="-1">經典的 GoF <a class="header-anchor" href="#經典的-gof" aria-label="Permalink to “經典的 GoF”">&#8203;</a></h3>
<p>1995 年由四位作者（Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides）出版的《Design Patterns》被譽為「軟體工程師的聖經」。
書中提出 23 種經典設計模式，這四位作者也因此被稱為 GoF (Gang of Four)。</p>
<p><strong>設計模式的三大類別：</strong></p>
<ol>
<li>
<p><strong>建立型模式（Creational Patterns）</strong> - 用於物件的創建</p>
<ul>
<li>工廠模式、抽象工廠模式、單例模式、建造者模式、原型模式</li>
</ul>
</li>
<li>
<p><strong>結構型模式（Structural Patterns）</strong> - 用於表示物件組合</p>
<ul>
<li>適配器模式、橋接模式、組合模式、裝飾者模式、外觀模式、享元模式、代理模式</li>
</ul>
</li>
<li>
<p><strong>行為型模式（Behavioral Patterns）</strong> - 用於物件之間的通訊</p>
<ul>
<li>責任鏈模式、命令模式、解釋器模式、迭代器模式、中介者模式、備忘錄模式、觀察者模式、狀態模式、策略模式、模板方法模式、訪問者模式</li>
</ul>
</li>
</ol>
<hr>
<h2 id="反模式-anti-pattern" tabindex="-1">反模式（Anti-Pattern） <a class="header-anchor" href="#反模式-anti-pattern" aria-label="Permalink to “反模式（Anti-Pattern）”">&#8203;</a></h2>
<p>反模式就像是一種模式，只不過它提供的不是解決方案，而是表面上看起來像解決方案但實際上不是解決方案的東西。</p>
<h3 id="為什麼會產生反模式" tabindex="-1">為什麼會產生反模式？ <a class="header-anchor" href="#為什麼會產生反模式" aria-label="Permalink to “為什麼會產生反模式？”">&#8203;</a></h3>
<ul>
<li><strong>經驗不足</strong>：不了解更好的做法</li>
<li><strong>時間壓力</strong>：趕著上線，先求有再求好</li>
<li><strong>誤用設計模式</strong>：把設計模式用在不適合的地方</li>
<li><strong>過度設計</strong>：為了用而用，反而把簡單問題複雜化</li>
</ul>
<p>反模式比不用設計模式更危險，因為它會讓程式碼品質下降，增加維護成本。有時候反模式會被誤認為是設計模式，這是最危險的情況。</p>
<h3 id="javascript-常見的反模式" tabindex="-1">JavaScript 常見的反模式 <a class="header-anchor" href="#javascript-常見的反模式" aria-label="Permalink to “JavaScript 常見的反模式”">&#8203;</a></h3>
<h4 id="_1-污染全域命名空間" tabindex="-1">1. 污染全域命名空間 <a class="header-anchor" href="#_1-污染全域命名空間" aria-label="Permalink to “1. 污染全域命名空間”">&#8203;</a></h4>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 反模式：所有變數都在全域</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'John'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> age </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 30</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> email </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'john@example.com'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 好的做法：使用模組或 IIFE</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'John'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  age: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">30</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  email: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'john@example.com'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><h4 id="_2-將字串傳給-settimeout-setinterval" tabindex="-1">2. 將字串傳給 setTimeout/setInterval <a class="header-anchor" href="#_2-將字串傳給-settimeout-setinterval" aria-label="Permalink to “2. 將字串傳給 setTimeout/setInterval”">&#8203;</a></h4>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 反模式：會觸發 eval()，不安全且效能差</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setTimeout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"console.log('這是不安全的！')"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 好的做法：傳遞函數</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setTimeout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'這是安全的！'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><h4 id="_3-修改內建原型" tabindex="-1">3. 修改內建原型 <a class="header-anchor" href="#_3-修改內建原型" aria-label="Permalink to “3. 修改內建原型”">&#8203;</a></h4>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 反模式：修改 Array.prototype</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">prototype</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">first</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 好的做法：使用工具函數</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getFirst</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">arr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> arr[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><h4 id="_4-不使用嚴格模式" tabindex="-1">4. 不使用嚴格模式 <a class="header-anchor" href="#_4-不使用嚴格模式" aria-label="Permalink to “4. 不使用嚴格模式”">&#8203;</a></h4>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 反模式：沒有 'use strict'，容易出錯</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculateTotal</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  totle </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 拼錯了，但不會報錯，變成全域變數</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 好的做法：啟用嚴格模式</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'use strict'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculateTotal</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> total</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 拼錯會立即報錯</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><h4 id="_5-callback-hell" tabindex="-1">5. Callback Hell <a class="header-anchor" href="#_5-callback-hell" aria-label="Permalink to “5. Callback Hell”">&#8203;</a></h4>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 反模式：多層嵌套的回呼</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  getMoreData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">b</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    getMoreData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(b, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">c</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(c)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 好的做法：使用 Promise 或 async/await</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> a</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> b</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getMoreData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> c</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getMoreData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(b)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(c)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br></div></div><h3 id="如何避免反模式" tabindex="-1">如何避免反模式？ <a class="header-anchor" href="#如何避免反模式" aria-label="Permalink to “如何避免反模式？”">&#8203;</a></h3>
<ol>
<li><strong>持續學習</strong>：了解最新的 Best Practices</li>
<li><strong>程式碼審查</strong>：讓團隊成員互相 Review 程式碼</li>
<li><strong>使用 Linter</strong>：透過工具自動檢查常見問題</li>
<li><strong>寫測試</strong>：好的測試能暴露設計問題，提高程式碼品質</li>
<li><strong>重構</strong>：定期重構程式碼，改善設計，避免累積技術債</li>
</ol>
<hr>
<h2 id="javascript-的模組化" tabindex="-1">JavaScript 的模組化 <a class="header-anchor" href="#javascript-的模組化" aria-label="Permalink to “JavaScript 的模組化”">&#8203;</a></h2>
<p>早期的 JavaScript 是沒有模組化概念的，歷經多年演進：</p>
<ol>
<li><strong>IIFE（即時函數）</strong> - 利用閉包封裝模組，形成私有空間</li>
<li><strong>CommonJS</strong> - Node.js 出現後的模組規範</li>
<li><strong>AMD（Asynchronous Module Definition）</strong> - 非同步模組定義</li>
<li><strong>UMD（Universal Module Definition）</strong> - 通用模組定義</li>
<li><strong>ES6 Modules</strong> - 現代 JavaScript 的模組化標準（<code>import</code> / <code>export</code>）</li>
</ol>
<p>如今結合 Vite、Webpack、Rollup 等打包工具，可以進行靜態分析和 Tree Shaking，讓 JavaScript 的模組化更加完善。</p>
<hr>
<h2 id="solid-原則" tabindex="-1">SOLID 原則 <a class="header-anchor" href="#solid-原則" aria-label="Permalink to “SOLID 原則”">&#8203;</a></h2>
<p>遵守這些原則，就算不懂設計模式，也能寫出好的程式碼。</p>
<h3 id="_1-srp-單一職責原則-single-responsibility-principle" tabindex="-1">1. SRP - 單一職責原則（Single Responsibility Principle） <a class="header-anchor" href="#_1-srp-單一職責原則-single-responsibility-principle" aria-label="Permalink to “1. SRP - 單一職責原則（Single Responsibility Principle）”">&#8203;</a></h3>
<p><strong>一個 Class 只負責一項職責</strong></p>
<p>在 Vue.js 中：</p>
<ul>
<li>將不同的功能分割到不同的元件中</li>
<li>確保每個元件只處理一個任務</li>
<li>提高元件的重用性</li>
</ul>
<p><strong>範例：</strong></p>
<p>假設有個購物車頁面，不應該把所有功能塞在同一個元件：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- ❌ 違反 SRP：一個元件做太多事 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>商品列表、價格計算、結帳按鈕、優惠券輸入...&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- ✅ 符合 SRP：拆分成多個元件 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">CartItemList</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> />      </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- 只負責顯示商品列表 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">CartSummary</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> />       </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- 只負責計算總價 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">CouponInput</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> />       </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- 只負責優惠券處理 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">CheckoutButton</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> />    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- 只負責結帳按鈕 --></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><h3 id="_2-ocp-開放封閉原則-open-closed-principle" tabindex="-1">2. OCP - 開放封閉原則（Open/Closed Principle） <a class="header-anchor" href="#_2-ocp-開放封閉原則-open-closed-principle" aria-label="Permalink to “2. OCP - 開放封閉原則（Open/Closed Principle）”">&#8203;</a></h3>
<p><strong>對於擴展是開放的，對於修改是封閉的</strong></p>
<p>能夠在不改變現有程式碼的情況下擴展元件的行為。</p>
<p>在 Vue.js 中：</p>
<ul>
<li>使用 <code>slots</code> 擴展元件功能</li>
<li>使用 <code>composables</code>（推薦）來封裝可重用的邏輯</li>
<li>避免使用 <code>mixins</code>（已不推薦）</li>
</ul>
<p><strong>範例：</strong></p>
<p>使用 slot 讓元件可以擴展，而不需要修改原始元件：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- ❌ 違反 OCP：每次新增需求都要改元件 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">Button</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> v-if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">type </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'primary'"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"btn-primary"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>送出&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">Button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">Button</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> v-else-if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">type </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'danger'"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"btn-danger"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>刪除&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">Button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- 每次新增按鈕類型都要改這裡 --></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- ✅ 符合 OCP：用 slot 擴展 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">Button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;template #icon>&#x3C;IconPlus />&#x3C;/template></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  新增項目</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">Button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- 可以自由擴展內容，不用改 Button 元件 --></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><h3 id="_3-lsp-里氏替換原則-liskov-substitution-principle" tabindex="-1">3. LSP - 里氏替換原則（Liskov Substitution Principle） <a class="header-anchor" href="#_3-lsp-里氏替換原則-liskov-substitution-principle" aria-label="Permalink to “3. LSP - 里氏替換原則（Liskov Substitution Principle）”">&#8203;</a></h3>
<p><strong>子類應該能夠替換它們的基類而不影響程式的正確性</strong></p>
<p>若使用 TypeScript，可定義共同的 <code>PaymentMethod</code> 介面，以確保所有支付策略都遵守相同結構。
(這裡的共同介面相當於 TypeScript 的 interface)</p>
<p>在 Vue.js 中：</p>
<ul>
<li>子類別物件（具體策略）可以在程式中取代其父類別物件（策略介面）</li>
<li>新增商品處理邏輯（新的策略）變得更加簡單，無需修改現有的元件邏輯</li>
</ul>
<p><strong>範例：</strong></p>
<p>不同的支付方式應該可以互換使用：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 所有支付方式都有相同的介面</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> paymentMethods</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  creditCard: { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pay</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">amount</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/* 信用卡支付 */</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  cash: { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pay</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">amount</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/* 現金支付 */</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  linePay: { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pay</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">amount</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/* LINE Pay 支付 */</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 可以自由切換，不影響程式運作</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> checkout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">method</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">amount</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  method.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pay</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(amount)  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 不管哪種支付方式，都能正常運作</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><h3 id="_4-isp-介面隔離原則-interface-segregation-principle" tabindex="-1">4. ISP - 介面隔離原則（Interface Segregation Principle） <a class="header-anchor" href="#_4-isp-介面隔離原則-interface-segregation-principle" aria-label="Permalink to “4. ISP - 介面隔離原則（Interface Segregation Principle）”">&#8203;</a></h3>
<p><strong>一個 Class（元件）不應該被強迫用到它不需要的方法</strong></p>
<p>在 Vue.js 中：</p>
<ul>
<li>設計細粒度的 <code>props</code> 和事件</li>
<li>確保元件不會被迫接收它們不需要的屬性或方法</li>
<li>提高元件的獨立性和重用性</li>
</ul>
<p><strong>範例：</strong></p>
<p>不要傳遞整個巨大的物件，只傳需要的屬性：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- ❌ 違反 ISP：傳整個 user 物件 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">UserCard</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> :</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- UserCard 可能只需要 name 和 avatar，卻被迫接收整個 user --></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- ✅ 符合 ISP：只傳需要的屬性 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">UserCard</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  :</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.name</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  :</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">avatar</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.avatar</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">/></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;!-- 元件只接收它真正需要的資料 --></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><h3 id="_5-dip-依賴反轉原則-dependency-inversion-principle" tabindex="-1">5. DIP - 依賴反轉原則（Dependency Inversion Principle） <a class="header-anchor" href="#_5-dip-依賴反轉原則-dependency-inversion-principle" aria-label="Permalink to “5. DIP - 依賴反轉原則（Dependency Inversion Principle）”">&#8203;</a></h3>
<p><strong>高階模組不該相依於低階模組，兩者都應該相依於抽象</strong></p>
<p>白話：不要把程式碼寫死在某種實作上</p>
<p>在 Vue.js 中：</p>
<ul>
<li>元件不直接管理狀態，而是透過 Pinia store 來管理</li>
<li>互動的部份交給抽象類別或介面，會改變的實作放到子類別裡面</li>
<li>未來需求改變，只需要修改 store 的實作，而不需要修改使用的元件</li>
</ul>
<p><strong>範例：</strong></p>
<p>元件不直接處理資料，而是相依於抽象的 store：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- ❌ 違反 DIP：元件直接處理 API 和資料 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> products</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([])</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchProducts</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> res</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/api/products'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 寫死實作</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  products.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> res.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">json</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- ✅ 符合 DIP：相依於抽象的 store --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useProductStore } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@/stores/product'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> productStore</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useProductStore</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 元件不知道資料怎麼來，只知道透過 store 取得</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 未來改用 GraphQL 或其他方式，元件不用改</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br></div></div><hr>
<h2 id="其他常見的設計模式" tabindex="-1">其他常見的設計模式 <a class="header-anchor" href="#其他常見的設計模式" aria-label="Permalink to “其他常見的設計模式”">&#8203;</a></h2>
<p>以下是幾個在 Vue.js 中常見的設計模式：</p>
<h3 id="_1-觀察者模式-observer-pattern" tabindex="-1">1. 觀察者模式（Observer Pattern） <a class="header-anchor" href="#_1-觀察者模式-observer-pattern" aria-label="Permalink to “1. 觀察者模式（Observer Pattern）”">&#8203;</a></h3>
<p><strong>當一個狀態改變時，所有相依於它的目標都會得到通知並自動更新。</strong></p>
<p>Vue.js 的響應式系統本質上就是一種觀察者模式的實現。當狀態變化時，Vue.js 會自動更新 DOM。</p>
<h3 id="_2-代理模式-proxy-pattern" tabindex="-1">2. 代理模式（Proxy Pattern） <a class="header-anchor" href="#_2-代理模式-proxy-pattern" aria-label="Permalink to “2. 代理模式（Proxy Pattern）”">&#8203;</a></h3>
<p><strong>為其他物件提供一種代理以控制對這個物件的存取。</strong></p>
<p>Vue 3 的 <code>reactive</code> 就是一種代理模式的實現，使用 JavaScript 的 Proxy API 來追蹤狀態變化。</p>
<h3 id="_3-module-模式" tabindex="-1">3. Module 模式 <a class="header-anchor" href="#_3-module-模式" aria-label="Permalink to “3. Module 模式”">&#8203;</a></h3>
<p><strong>用來模擬類別的私有成員，並且可以將公開的成員封裝在閉包中。</strong></p>
<p>Vuex 或 Pinia 結合了 Module、Observer 與 Singleton 模式，用以實作集中式的狀態管理。</p>
<h3 id="_4-工廠方法模式-factory-method-pattern" tabindex="-1">4. 工廠方法模式（Factory Method Pattern） <a class="header-anchor" href="#_4-工廠方法模式-factory-method-pattern" aria-label="Permalink to “4. 工廠方法模式（Factory Method Pattern）”">&#8203;</a></h3>
<p><strong>在不直接調用構造函數的情況下建立物件。</strong></p>
<p>在 Vue.js 中，可透過 Props 或 Attr 來決定要渲染的元件、樣式等：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> :</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">is</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">currentView</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p><code>&lt;component :is&gt;</code> 屬於 Vue 的動態元件機制，本質是透過工廠方法的概念實現動態建立與切換元件。
根據不同條件動態切換元件，這就是工廠模式在 Vue.js 的應用。</p>
<h3 id="_5-單例模式-singleton-pattern" tabindex="-1">5. 單例模式（Singleton Pattern） <a class="header-anchor" href="#_5-單例模式-singleton-pattern" aria-label="Permalink to “5. 單例模式（Singleton Pattern）”">&#8203;</a></h3>
<p><strong>確保一個類別只有一個實例，並且讓全域都能存取。</strong></p>
<p>全域狀態管理（如 Pinia store）就是單例模式的典型應用，確保整個應用程式共享同一個狀態實例。</p>
<h3 id="_6-策略模式-strategy-pattern" tabindex="-1">6. 策略模式（Strategy Pattern） <a class="header-anchor" href="#_6-策略模式-strategy-pattern" aria-label="Permalink to “6. 策略模式（Strategy Pattern）”">&#8203;</a></h3>
<p><strong>定義一系列的算法，把它們一個個封裝起來，並且使它們可以互相替換。</strong></p>
<p>例如表單驗證時，可以定義多種驗證策略（email 驗證、長度驗證、必填驗證等），根據需求組合使用。</p>
<h3 id="_7-命令模式-command-pattern" tabindex="-1">7. 命令模式（Command Pattern） <a class="header-anchor" href="#_7-命令模式-command-pattern" aria-label="Permalink to “7. 命令模式（Command Pattern）”">&#8203;</a></h3>
<p><strong>將請求封裝為物件，藉由不同的請求來參數化其他物件。</strong></p>
<p>常見於實作 Undo/Redo 功能，將每個操作封裝成命令物件，就能輕鬆實現這類功能。</p>
<h3 id="_8-責任鏈模式-chain-of-responsibility-pattern" tabindex="-1">8. 責任鏈模式（Chain of Responsibility Pattern） <a class="header-anchor" href="#_8-責任鏈模式-chain-of-responsibility-pattern" aria-label="Permalink to “8. 責任鏈模式（Chain of Responsibility Pattern）”">&#8203;</a></h3>
<p><strong>使多個物件都有機會處理請求，從而避免請求的發送者和接收者之間的耦合關係。</strong></p>
<p>例如一連串的驗證規則，按順序對輸入進行驗證，直到某規則回傳驗證失敗，或者所有條件都通過。</p>
<hr>
<h2 id="複合模式" tabindex="-1">複合模式 <a class="header-anchor" href="#複合模式" aria-label="Permalink to “複合模式”">&#8203;</a></h2>
<p>實務上分析需求時，常常會發現要同時結合多種模式才能解決問題。</p>
<ul>
<li>不見得一個問題只能用一種模式</li>
<li>分析需求時，通常會發現要結合多個模式才能解決問題</li>
<li>有些模式是多個基本模式所結合出來的，就被稱為複合模式</li>
<li>例如傳統 MVC 就會用到 Strategy 模式、Observer 模式、Composite 模式等</li>
</ul>
<hr>
<h2 id="總結" tabindex="-1">總結 <a class="header-anchor" href="#總結" aria-label="Permalink to “總結”">&#8203;</a></h2>
<p>整理完這篇筆記，回想當時讀書會的討論，最大的收穫不是記住了多少種設計模式，而是理解了「為什麼要用」比「怎麼用」更重要。</p>
<p>設計模式不是銀彈，也不是炫技的工具，它最大的價值在於：</p>
<ol>
<li><strong>提供共通的語言</strong> - 跟同事說「這裡用觀察者模式」，大家立刻知道你在講什麼</li>
<li><strong>前人的經驗總結</strong> - 避免重蹈覆轍，站在巨人的肩膀上</li>
<li><strong>幫助思考架構</strong> - 面對複雜問題時，有個思考的框架</li>
</ol>
<p><strong>但要記住幾個重點：</strong></p>
<ul>
<li><strong>先求簡單</strong>： 不要為了用設計模式而用，先解決問題再說</li>
<li><strong>遵守 SOLID</strong>：就算不懂設計模式，遵守 SOLID 原則也能寫出好程式碼</li>
<li><strong>避免過度設計</strong>： 過早最佳化 (Premature Optimization) 是萬惡的根源</li>
<li><strong>別被名詞困住</strong>：看到自己寫的程式碼符合某個模式的精神，恭喜你已經在用了!</li>
</ul>
<p><strong>給想學設計模式的朋友：</strong></p>
<ol>
<li>不用急著全部記起來，先從常用的開始（觀察者、工廠、單例）</li>
<li>在實際專案中練習，看到問題自然會想到對應的模式</li>
<li>多看別人的程式碼，尤其是開源專案（Vue、React 等框架的原始碼都是設計模式的寶庫）</li>
<li>持續重構，設計模式不是一次到位，而是持續改進的過程</li>
</ol>
<p>最後，寫程式跟寫文章一樣，重點是讓讀者（包含未來的自己）容易理解。設計模式是幫助我們達成這個目標的工具，而不是目標本身。</p>
<hr>
<p><strong>相關資源：</strong></p>
<ul>
<li><a href="https://www.tenlong.com.tw/products/9786263247123" target="_blank" rel="noreferrer">JavaScript 設計模式學習手冊</a></li>
<li><a href="https://vuejs.org/" target="_blank" rel="noreferrer">Vue.js 官方文件</a></li>
<li><a href="https://pinia.vuejs.org/" target="_blank" rel="noreferrer">Pinia 官方文件</a></li>
</ul>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>javascript</category>
            <category>design-patterns</category>
            <category>solid</category>
            <category>vue</category>
        </item>
        <item>
            <title><![CDATA[用 VitePress Rewrites 按日期整理原始 markdown]]></title>
            <link>https://kurohsu.dev/notes/organizing-vitepress-articles-by-date.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/organizing-vitepress-articles-by-date.html</guid>
            <pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="用-vitepress-rewrites-按日期整理原始-markdown" tabindex="-1">用 VitePress Rewrites 按日期整理原始 markdown <a class="header-anchor" href="#用-vitepress-rewrites-按日期整理原始-markdown" aria-label="Permalink to “用 VitePress Rewrites 按日期整理原始 markdown”">&#8203;</a></h1>
<p>在轉移舊文章到 VitePress 部落格的過程中，遇到了一個檔案管理的挑戰，隨著部落格文章越來越多，檔案管理開始變得有點頭痛。
好幾十篇文章全部平鋪在同一個目錄下，每次想找特定時期的文章都要捲半天。
但如果把文章按日期分到子目錄，URL 又會變得很醜，重新生成 URL 也會影響 SEO。</p>
<p>後來發現 VitePress 的 rewrites 功能可以完美解決這個問題，這篇文章就來聊聊實作過程。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="用-vitepress-rewrites-按日期整理原始-markdown" tabindex="-1">用 VitePress Rewrites 按日期整理原始 markdown <a class="header-anchor" href="#用-vitepress-rewrites-按日期整理原始-markdown" aria-label="Permalink to “用 VitePress Rewrites 按日期整理原始 markdown”">&#8203;</a></h1>
<p>在轉移舊文章到 VitePress 部落格的過程中，遇到了一個檔案管理的挑戰，隨著部落格文章越來越多，檔案管理開始變得有點頭痛。
好幾十篇文章全部平鋪在同一個目錄下，每次想找特定時期的文章都要捲半天。
但如果把文章按日期分到子目錄，URL 又會變得很醜，重新生成 URL 也會影響 SEO。</p>
<p>後來發現 VitePress 的 rewrites 功能可以完美解決這個問題，這篇文章就來聊聊實作過程。</p>
<hr>
<h2 id="問題-檔案太多不好管理" tabindex="-1">問題：檔案太多不好管理 <a class="header-anchor" href="#問題-檔案太多不好管理" aria-label="Permalink to “問題：檔案太多不好管理”">&#8203;</a></h2>
<p>原本的檔案結構是這樣：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>docs/notes/</span></span>
<span class="line"><span>├── integrating-waline-comments-to-vitepress.md</span></span>
<span class="line"><span>├── javascript-clean-code-practices.md</span></span>
<span class="line"><span>├── vue3-composition-api-patterns.md</span></span>
<span class="line"><span>├── ecmascript-5-strict-mode.md </span></span>
<span class="line"><span>├── ... (略)</span></span>
<span class="line"><span>└── index.md</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><p><strong>遇到的問題：</strong></p>
<ol>
<li><strong>檔案列表太長</strong>：IDE 的檔案樹要滾很久才能找到想要的檔案</li>
<li><strong>不知道文章年份</strong>：光看檔名很難知道這是新文章還是舊文章</li>
<li><strong>難以批次處理</strong>：想要對特定時期的文章做處理（例如更新 frontmatter）很麻煩</li>
</ol>
<hr>
<h2 id="考慮過的方案" tabindex="-1">考慮過的方案 <a class="header-anchor" href="#考慮過的方案" aria-label="Permalink to “考慮過的方案”">&#8203;</a></h2>
<h3 id="方案-1-檔名加日期前綴" tabindex="-1">方案 1：檔名加日期前綴 <a class="header-anchor" href="#方案-1-檔名加日期前綴" aria-label="Permalink to “方案 1：檔名加日期前綴”">&#8203;</a></h3>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>docs/notes/</span></span>
<span class="line"><span>├── 2025-11-04-integrating-waline-comments.md</span></span>
<span class="line"><span>├── 2025-10-31-javascript-clean-code-practices.md</span></span>
<span class="line"><span>└── 2011-11-27-ecmascript-5-strict-mode.md</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p><strong>優點：</strong></p>
<ul>
<li>實作超簡單，不用改任何設定</li>
<li>檔案列表自動依照日期排序</li>
</ul>
<p><strong>缺點：</strong></p>
<ul>
<li>URL 變長：<code>/notes/2025-11-04-integrating-waline-comments.html</code></li>
<li>日期資訊重複（frontmatter 已經有了）</li>
<li>檔名太長，不好辨識重點</li>
</ul>
<h3 id="方案-2-完全依照年月分類" tabindex="-1">方案 2：完全依照年月分類 <a class="header-anchor" href="#方案-2-完全依照年月分類" aria-label="Permalink to “方案 2：完全依照年月分類”">&#8203;</a></h3>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>docs/notes/2025/11/integrating-waline-comments.md</span></span>
<span class="line"><span>→ URL: /notes/2025/11/integrating-waline-comments.html</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p><strong>優點：</strong></p>
<ul>
<li>檔案結構清晰</li>
</ul>
<p><strong>缺點：</strong></p>
<ul>
<li>URL 層級太深，看起來冗長</li>
<li>所有現有 URL 都會改變，破壞 SEO</li>
<li>如果有內部參考連結也要更新</li>
</ul>
<h3 id="方案-3-vitepress-rewrites" tabindex="-1">方案 3：VitePress Rewrites <a class="header-anchor" href="#方案-3-vitepress-rewrites" aria-label="Permalink to “方案 3：VitePress Rewrites”">&#8203;</a></h3>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>檔案: docs/notes/2025/11/integrating-waline-comments.md</span></span>
<span class="line"><span>URL:  /notes/integrating-waline-comments.html</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p><strong>這就是我要的！</strong></p>
<ul>
<li>原始的 md 檔案依照年月分類，方便管理</li>
<li>URL 保持簡潔扁平</li>
<li>完全相容（原有的 URL 不受影響）</li>
</ul>
<hr>
<h2 id="實作步驟" tabindex="-1">實作步驟 <a class="header-anchor" href="#實作步驟" aria-label="Permalink to “實作步驟”">&#8203;</a></h2>
<h3 id="第一步-設定-rewrites" tabindex="-1">第一步：設定 Rewrites <a class="header-anchor" href="#第一步-設定-rewrites" aria-label="Permalink to “第一步：設定 Rewrites”">&#8203;</a></h3>
<p>在 <code>docs/.vitepress/config.ts</code> 加入 rewrites 規則：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // Rewrites: 將年月目錄結構的文章對外呈現為扁平 URL</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  rewrites: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    'notes/:year/:month/:article.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'notes/:article.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    'learn/:year/:month/:article.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'learn/:article.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    'misc/:year/:month/:article.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'misc/:article.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 其他設定...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><p>這個規則的意思是：</p>
<ul>
<li>實際檔案路徑：<code>notes/2025/11/my-article.md</code></li>
<li>對外 URL：<code>/notes/my-article.html</code></li>
</ul>
<p>在打包的時候 VitePress 會自動處理這個對應關係。</p>
<hr>
<h3 id="第二步-更新-data-loader" tabindex="-1">第二步：更新 Data Loader <a class="header-anchor" href="#第二步-更新-data-loader" aria-label="Permalink to “第二步：更新 Data Loader”">&#8203;</a></h3>
<p>這一步有兩個重點：</p>
<h4 id="_1-確認掃描路徑包含子目錄" tabindex="-1">1. 確認掃描路徑包含子目錄 <a class="header-anchor" href="#_1-確認掃描路徑包含子目錄" aria-label="Permalink to “1. 確認掃描路徑包含子目錄”">&#8203;</a></h4>
<p>我的 <code>posts.data.ts</code> 原本就用 <code>**/*.md</code> 來掃描，所以不需要改：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createContentLoader</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'notes/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ** 會遞迴掃描所有子目錄</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'learn/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'misc/**/*.md'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  excerpt: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  transform</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">raw</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[] {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 資料處理...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><p>如果你的掃描路徑是寫死的（例如 <code>notes/*.md</code>），記得改成 <code>notes/**/*.md</code>。</p>
<h4 id="_2-轉換-url-為扁平結構" tabindex="-1">2. 轉換 URL 為扁平結構 <a class="header-anchor" href="#_2-轉換-url-為扁平結構" aria-label="Permalink to “2. 轉換 URL 為扁平結構”">&#8203;</a></h4>
<p><strong>重要！</strong> VitePress 的 data loader 會根據實際檔案路徑生成 URL，所以需要手動轉換：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 將年月目錄結構的 URL 轉換為扁平 URL（對應 VitePress rewrites 設定）</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 範例: /notes/2025/11/article.html → /notes/article.html</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> rewriteUrl</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">url</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 匹配格式: /(notes|learn|misc)/YYYY/MM/article.html</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> match</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> url.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">match</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">(notes</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">|</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">learn</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">|</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">misc)</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\d</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">{4}</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\d</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">{2}</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">html)</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (match) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">category</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">filename</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> match</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `/${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">category</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">filename</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 如果不符合年月格式，直接返回原 URL</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> url</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createContentLoader</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  excerpt: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  transform</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">raw</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[] {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> raw</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">url</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">frontmatter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">excerpt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        title: frontmatter.title,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        url: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">rewriteUrl</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(url),  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ← 轉換 URL</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        excerpt,</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br></div></div><p>如果不加這個轉換，首頁和分類頁的連結會指向 <code>/notes/2025/11/article.html</code>，導致 404 錯誤。</p>
<hr>
<h3 id="第三步-建立轉移的-script" tabindex="-1">第三步：建立轉移的 script <a class="header-anchor" href="#第三步-建立轉移的-script" aria-label="Permalink to “第三步：建立轉移的 script”">&#8203;</a></h3>
<p>這裡我寫了一個 script 來自動搬移所有文章到正確的年、月目錄。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> fs</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'fs'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> path</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'path'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> matter</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'gray-matter'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> categories</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'notes'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'learn'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'misc'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">categories.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">forEach</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">category</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> categoryDir</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'docs'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, category);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> files</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">readdirSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(categoryDir)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">file</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> file.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">endsWith</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> file </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'index.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  files.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">forEach</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">filename</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> sourcePath</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(categoryDir, filename);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> content</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">readFileSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(sourcePath, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'utf-8'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> matter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(content);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">data.date) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`跳過（無日期）: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">filename</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 解析日期</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> date</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(data.date);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> year</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getFullYear</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> month</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getMonth</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">padStart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'0'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 建立目標路徑</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> targetDir</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(categoryDir, </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">String</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(year), month);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> targetPath</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(targetDir, filename);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 建立目錄並移動檔案</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">existsSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(targetDir)) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">mkdirSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(targetDir, { recursive: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">renameSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(sourcePath, targetPath);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`已移動: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">filename</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">} → ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">year</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">month</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br></div></div><hr>
<h2 id="最終效果" tabindex="-1">最終效果 <a class="header-anchor" href="#最終效果" aria-label="Permalink to “最終效果”">&#8203;</a></h2>
<h3 id="開發環境-檔案結構" tabindex="-1">開發環境（檔案結構） <a class="header-anchor" href="#開發環境-檔案結構" aria-label="Permalink to “開發環境（檔案結構）”">&#8203;</a></h3>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>docs/notes/</span></span>
<span class="line"><span>├── 2025/</span></span>
<span class="line"><span>│   └── 11/</span></span>
<span class="line"><span>│       ├── integrating-waline-comments-to-vitepress.md</span></span>
<span class="line"><span>│       ├── javascript-clean-code-practices.md</span></span>
<span class="line"><span>│       └── vite-optimization-guide.md</span></span>
<span class="line"><span>├── 2017/</span></span>
<span class="line"><span>│   ├── 09/</span></span>
<span class="line"><span>│   ├── 08/</span></span>
<span class="line"><span>│   └── 07/</span></span>
<span class="line"><span>├── 2015/</span></span>
<span class="line"><span>│   ├── 05/ (6 篇文章)</span></span>
<span class="line"><span>│   ├── 04/</span></span>
<span class="line"><span>│   └── ...</span></span>
<span class="line"><span>└── index.md</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br></div></div><p><strong>優點：</strong></p>
<ul>
<li>IDE 側邊欄可以依照年份折疊，方便瀏覽</li>
<li>可以快速找到特定年份或月份的文章</li>
<li>新增文章時很清楚該放哪裡</li>
</ul>
<h3 id="正式環境-url-結構" tabindex="-1">正式環境（URL 結構） <a class="header-anchor" href="#正式環境-url-結構" aria-label="Permalink to “正式環境（URL 結構）”">&#8203;</a></h3>
<p>所有 URL 保持簡潔扁平：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>/notes/integrating-waline-comments-to-vitepress.html</span></span>
<span class="line"><span>/notes/javascript-clean-code-practices.html</span></span>
<span class="line"><span>/notes/ecmascript-5-strict-mode.html</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p><strong>優點：</strong></p>
<ul>
<li>URL 簡短易記</li>
<li>對 SEO Friendly</li>
<li>舊的分享連結完全不受影響</li>
</ul>
<hr>
<h2 id="新增文章的流程" tabindex="-1">新增文章的流程 <a class="header-anchor" href="#新增文章的流程" aria-label="Permalink to “新增文章的流程”">&#8203;</a></h2>
<p>以後新增文章只需要：</p>
<ol>
<li>
<p><strong>確認日期</strong>：例如今天是 2025-11-06</p>
</li>
<li>
<p><strong>建立年月目錄</strong>（如果不存在）：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">mkdir</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -p</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> docs/notes/2025/11</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div></li>
<li>
<p><strong>建立文章檔案</strong>：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">docs/notes/2025/11/my-new-article.md</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div></li>
<li>
<p><strong>寫入 Frontmatter</strong>：</p>
<div class="language-markdown line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">markdown</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">我的新文章</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">date</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2025-11-06</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tags</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">vitepress</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">---</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div></li>
<li>
<p><strong>完成！</strong> VitePress 會自動：</p>
<ul>
<li>掃描到這篇文章</li>
<li>產生 <code>/notes/my-new-article.html</code> URL</li>
<li>加入文章列表和標籤頁面</li>
</ul>
</li>
</ol>
<hr>
<h2 id="注意事項" tabindex="-1">注意事項 <a class="header-anchor" href="#注意事項" aria-label="Permalink to “注意事項”">&#8203;</a></h2>
<h3 id="_1-檔名不能重複" tabindex="-1">1. 檔名不能重複 <a class="header-anchor" href="#_1-檔名不能重複" aria-label="Permalink to “1. 檔名不能重複”">&#8203;</a></h3>
<p>因為 URL 是扁平的，所以即使在不同年月目錄，檔名也不能重複。</p>
<p><strong>錯誤示範：</strong></p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>docs/notes/2025/11/vue-tips.md</span></span>
<span class="line"><span>docs/notes/2024/05/vue-tips.md  ← 衝突！</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p>兩個檔案都會產生 <code>/notes/vue-tips.html</code>，後者會覆蓋前者。</p>
<p><strong>正確做法：</strong></p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>docs/notes/2025/11/vue-composition-api-tips.md</span></span>
<span class="line"><span>docs/notes/2024/05/vue-options-api-tips.md</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><h3 id="_2-data-loader-需要重啟" tabindex="-1">2. Data Loader 需要重啟 <a class="header-anchor" href="#_2-data-loader-需要重啟" aria-label="Permalink to “2. Data Loader 需要重啟”">&#8203;</a></h3>
<p>修改 frontmatter 或新增檔案後，需要重啟開發伺服器：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># Ctrl+C 停止，然後重新啟動</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> dev</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p>VitePress 的 data loader 會快取結果，不重啟的話看不到新文章。</p>
<h3 id="_3-tag-路由也要更新掃描路徑" tabindex="-1">3. Tag 路由也要更新掃描路徑 <a class="header-anchor" href="#_3-tag-路由也要更新掃描路徑" aria-label="Permalink to “3. Tag 路由也要更新掃描路徑”">&#8203;</a></h3>
<p>如果你有動態標籤頁面（<code>docs/tags/[tag].paths.ts</code>），確認掃描路徑有包含所有子目錄：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createContentLoader</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'notes/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ← 確保有 **</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'learn/**/*.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'misc/**/*.md'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">load</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><hr>
<h2 id="總結" tabindex="-1">總結 <a class="header-anchor" href="#總結" aria-label="Permalink to “總結”">&#8203;</a></h2>
<p>VitePress 的 rewrites 功能真的很強大，讓我可以：</p>
<ul>
<li><strong>在開發時</strong>享受有組織的檔案結構</li>
<li><strong>在正式環境</strong>保持簡潔的 URL</li>
<li><strong>完全相容</strong>，不會破壞任何現有連結</li>
</ul>
<p>這次重構花了大約一小時，包含寫轉移的 script、測試、更新文件。
如果你的 VitePress 部落格也有檔案管理問題，非常推薦試試這個方法！</p>
<hr>
<p><strong>相關閱讀：</strong></p>
<ul>
<li><a href="https://vitepress.dev/guide/routing#rewrites" target="_blank" rel="noreferrer">VitePress 官方文件 - Rewrites</a></li>
<li><a href="/notes/integrating-waline-comments-to-vitepress.html">為 VitePress 部落格加上 Waline 留言系統</a></li>
</ul>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vitepress</category>
            <category>vitepress路由</category>
        </item>
        <item>
            <title><![CDATA[關於這個部落格的二三事]]></title>
            <link>https://kurohsu.dev/misc/about-this-blog.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/misc/about-this-blog.html</guid>
            <pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="關於這個部落格的二三事" tabindex="-1">關於這個部落格的二三事 <a class="header-anchor" href="#關於這個部落格的二三事" aria-label="Permalink to “關於這個部落格的二三事”">&#8203;</a></h1>
<p>如果你曾經造訪過 <a href="http://kuro.tw" target="_blank" rel="noreferrer">kuro.tw</a>，會發現那個站點已經好幾年沒有更新了。不是我懶（好吧，確實有點懶），而是當時用的 Hexo 加上一堆客製化調整，每次想寫點東西都要先處理環境問題，久而久之就<del>放生</del>擱置了。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="關於這個部落格的二三事" tabindex="-1">關於這個部落格的二三事 <a class="header-anchor" href="#關於這個部落格的二三事" aria-label="Permalink to “關於這個部落格的二三事”">&#8203;</a></h1>
<p>如果你曾經造訪過 <a href="http://kuro.tw" target="_blank" rel="noreferrer">kuro.tw</a>，會發現那個站點已經好幾年沒有更新了。不是我懶（好吧，確實有點懶），而是當時用的 Hexo 加上一堆客製化調整，每次想寫點東西都要先處理環境問題，久而久之就<del>放生</del>擱置了。</p>
<hr>
<h2 id="新的開始" tabindex="-1">新的開始 <a class="header-anchor" href="#新的開始" aria-label="Permalink to “新的開始”">&#8203;</a></h2>
<p>最近剛好入手了新網域 <code>kurohsu.dev</code>，想說既然都有新網域了，就順勢把部落格重開起來吧。這次用 VitePress 重新搭建，也把過去的文章都搬過來了。</p>
<p>除了技術文章之外，這次也想寫些非技術類的內容。像是秋葉原女僕咖啡廳的所見所得 XD、球場當大炮哥的經驗之類的。畢竟人生不是只有 code，偶爾記錄一些有的沒的也挺有趣。</p>
<h2 id="舊文章遷移" tabindex="-1">舊文章遷移 <a class="header-anchor" href="#舊文章遷移" aria-label="Permalink to “舊文章遷移”">&#8203;</a></h2>
<p>把 kuro.tw 上的舊文章都搬過來了，翻出來一看才發現當時寫了不少東西：</p>
<ul>
<li><strong>Vue.js 相關</strong> - 那個年代 Vue 2 還很新鮮，現在都 Vue 3 了</li>
<li><strong>D3.js 相關</strong> - 視覺化開發的一些心得和 demo</li>
<li><strong>Google Maps API</strong> - 各種地圖應用的嘗試</li>
<li><strong>JavaScript 基礎</strong> - 一些語言特性的筆記</li>
</ul>
<p>看著這些舊文章有種時光倒流的感覺。有些技術現在已經過時了，但當時的思考過程還是值得保留的。</p>
<h2 id="技術實作" tabindex="-1">技術實作 <a class="header-anchor" href="#技術實作" aria-label="Permalink to “技術實作”">&#8203;</a></h2>
<p>如果你對這個站點的技術細節有興趣：</p>
<ul>
<li><strong>框架</strong>: VitePress 2.0</li>
<li><strong>樣式</strong>: Tailwind CSS + 客製化主題</li>
<li><strong>部署</strong>: Vercel（推送就自動部署，真香）</li>
</ul>
<p>自幹標籤系統、分頁功能、響應式設計這些該有的都有了，基本上可以正常運作。</p>
<h2 id="接下來想做的事" tabindex="-1">接下來想做的事 <a class="header-anchor" href="#接下來想做的事" aria-label="Permalink to “接下來想做的事”">&#8203;</a></h2>
<p>沒有什麼宏偉的計畫，就是：</p>
<ul>
<li>繼續寫技術筆記（Vue 3、前端工具、AI 開發相關等等的）</li>
<li>多寫點生活類的文章（不只有技術）</li>
<li>寫個 AI Plugin 來輔助文章撰寫，看能不能增加文章產量 (Vibe blogging ?)</li>
<li>看看要不要加個留言系統（還在想用什麼方案）</li>
</ul>
<p>能做多少算多少，反正是自己的 Blog 也不趕時間。</p>
<h2 id="結語" tabindex="-1">結語 <a class="header-anchor" href="#結語" aria-label="Permalink to “結語”">&#8203;</a></h2>
<p>寫部落格對我來說就是一種整理思緒的方式，技術筆記幫助自己記錄學習過程，生活隨筆則是留下一些當下的想法。</p>
<p>如果你在瀏覽的過程中發現任何問題，歡迎跟我說。畢竟站點還在持續調整，<del>Bug 是特色不是問題</del>。</p>
<p>歡迎來到新的 kurohsu.dev！</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>部落格</category>
            <category>VitePress</category>
            <category>遷移</category>
        </item>
        <item>
            <title><![CDATA[為 VitePress 部落格加上 Waline 留言系統]]></title>
            <link>https://kurohsu.dev/notes/integrating-waline-comments-to-vitepress.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/integrating-waline-comments-to-vitepress.html</guid>
            <pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="為-vitepress-部落格加上-waline-留言系統" tabindex="-1">為 VitePress 部落格加上 Waline 留言系統 <a class="header-anchor" href="#為-vitepress-部落格加上-waline-留言系統" aria-label="Permalink to “為 VitePress 部落格加上 Waline 留言系統”">&#8203;</a></h1>
<p>建好站台之後就在想要不要幫部落格加個留言功能。
畢竟技術文章有時候真的需要跟讀者討論交流，但又不想搞得太複雜。
考慮過幾個方案後，最後選擇了 Waline，這篇文章就來聊聊為什麼選它，以及實際整合的過程。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="為-vitepress-部落格加上-waline-留言系統" tabindex="-1">為 VitePress 部落格加上 Waline 留言系統 <a class="header-anchor" href="#為-vitepress-部落格加上-waline-留言系統" aria-label="Permalink to “為 VitePress 部落格加上 Waline 留言系統”">&#8203;</a></h1>
<p>建好站台之後就在想要不要幫部落格加個留言功能。
畢竟技術文章有時候真的需要跟讀者討論交流，但又不想搞得太複雜。
考慮過幾個方案後，最後選擇了 Waline，這篇文章就來聊聊為什麼選它，以及實際整合的過程。</p>
<hr>
<h2 id="為什麼選-waline" tabindex="-1">為什麼選 Waline？ <a class="header-anchor" href="#為什麼選-waline" aria-label="Permalink to “為什麼選 Waline？”">&#8203;</a></h2>
<p>在決定用哪套留言系統之前，我其實看了好幾個選項：</p>
<p><strong>Disqus</strong> 雖然老牌但太重了，而且免費版有廣告，隱私問題也讓人不太放心。</p>
<p><strong>Giscus</strong> 基於 GitHub Discussions，對技術部落格來說很棒，但問題是不是每個讀者都有 GitHub 帳號。
我自己寫的內容有技術文也有生活隨筆，如果只有 GitHub 用戶能留言，感覺門檻有點高。</p>
<p>最後選了 <strong>Waline</strong> 的原因很簡單：</p>
<ol>
<li><strong>支援匿名留言</strong>：讀者可以直接留言，不用強制登入</li>
<li><strong>多種登入方式</strong>：想登入的話也支援 GitHub、Google 等社群帳號</li>
<li><strong>繁體中文支援完善</strong>：介面和提示都有繁中翻譯</li>
<li><strong>無後端煩惱</strong>：後端可以部署在 Vercel，跟部落格用同一個平台</li>
<li><strong>功能完整</strong>：表情、Markdown、圖片上傳、郵件通知都有</li>
</ol>
<hr>
<h2 id="實作步驟" tabindex="-1">實作步驟 <a class="header-anchor" href="#實作步驟" aria-label="Permalink to “實作步驟”">&#8203;</a></h2>
<h3 id="第一步-安裝-waline-用戶端套件" tabindex="-1">第一步：安裝 Waline 用戶端套件 <a class="header-anchor" href="#第一步-安裝-waline-用戶端套件" aria-label="Permalink to “第一步：安裝 Waline 用戶端套件”">&#8203;</a></h3>
<p>首先要把 Waline 的用戶端套件裝起來。</p>
<p>我的專案用的是 pnpm：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> @waline/client</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>如果你用 npm 或 yarn：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> @waline/client</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># or</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">yarn</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> @waline/client</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><hr>
<h3 id="第二步-建立-waline-vue-元件" tabindex="-1">第二步：建立 Waline Vue 元件 <a class="header-anchor" href="#第二步-建立-waline-vue-元件" aria-label="Permalink to “第二步：建立 Waline Vue 元件”">&#8203;</a></h3>
<p>接下來要建立一個 Vue 元件來包裝 Waline。</p>
<p>在 VitePress 專案中，我把它放在 <code>docs/.vitepress/theme/components/WalineComment.vue</code>：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ts"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { onMounted, onUnmounted, watch, ref } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useRoute, useData } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vitepress'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { init } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@waline/client'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@waline/client/style'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> route</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useRoute</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">isDark</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> walineInstance</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">any</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Waline 設定</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> walineOptions</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  el: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#waline-comments'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  serverURL: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://your-waline-server.vercel.app'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 替換成你的後端 URL</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  lang: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'zh-TW'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  locale: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    placeholder: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'留下你的想法...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // ... 其他繁體中文翻譯</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  dark: isDark.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'html.dark'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> :</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'auto'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  meta: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'nick'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'mail'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'link'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  requiredMeta: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'nick'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  pageSize: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  emoji: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    'https://cdn.jsdelivr.net/npm/@waline/emojis@1.2.0/weibo'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    'https://cdn.jsdelivr.net/npm/@waline/emojis@1.2.0/bilibili'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  pageview: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  comment: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 初始化</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">onMounted</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  walineInstance.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> init</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(walineOptions)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 路由變化時更新留言</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">watch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> route.path, () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (walineInstance.value) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    walineInstance.value.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 深色模式切換</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">watch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(isDark, (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">newValue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (walineInstance.value) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    walineInstance.value.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      dark: newValue </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'html.dark'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> :</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'auto'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 清理</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">onUnmounted</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (walineInstance.value) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    walineInstance.value.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">destroy</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"waline-wrapper"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"waline-container"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h2</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"waline-title"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>💬 留言討論&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"waline-comments"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br><span class="line-number">55</span><br><span class="line-number">56</span><br><span class="line-number">57</span><br><span class="line-number">58</span><br><span class="line-number">59</span><br><span class="line-number">60</span><br><span class="line-number">61</span><br><span class="line-number">62</span><br><span class="line-number">63</span><br><span class="line-number">64</span><br><span class="line-number">65</span><br><span class="line-number">66</span><br><span class="line-number">67</span><br><span class="line-number">68</span><br></div></div><p>這個元件做了幾件重要的事：</p>
<ol>
<li><strong>繁體中文設定</strong>：<code>lang: 'zh-TW'</code> 和 <code>locale</code> 物件確保介面是繁中</li>
<li><strong>深色模式支援</strong>：監聽 VitePress 的 <code>isDark</code> 狀態，自動切換佈景主題</li>
<li><strong>路由更新</strong>：當切換文章時，留言區會自動更新</li>
<li><strong>生命週期管理</strong>：確保元件銷毀時清理 Waline 實體</li>
</ol>
<hr>
<h3 id="第三步-註冊元件" tabindex="-1">第三步：註冊元件 <a class="header-anchor" href="#第三步-註冊元件" aria-label="Permalink to “第三步：註冊元件”">&#8203;</a></h3>
<p>在 <code>docs/.vitepress/theme/index.ts</code> 中註冊這個元件：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> WalineComment </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './components/WalineComment.vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  extends: DefaultTheme,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  Layout,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  enhanceApp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">app</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    app.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'WalineComment'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, WalineComment)</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // ... 其他元件</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><hr>
<h3 id="第四步-整合到文章頁面" tabindex="-1">第四步：整合到文章頁面 <a class="header-anchor" href="#第四步-整合到文章頁面" aria-label="Permalink to “第四步：整合到文章頁面”">&#8203;</a></h3>
<p>接著要在文章頁面底部加上留言區。</p>
<p>我的做法是修改 <code>docs/.vitepress/theme/Layout.vue</code>，使用 VitePress 的 <code>#doc-after</code> slot：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">Layout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> #</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">doc-after</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      &#x3C;!-- 只在文章頁面顯示留言 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">WalineComment</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"isArticlePage &#x26;&#x26; frontmatter.comment !== false"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">Layout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ts"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { useData } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vitepress'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { computed } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">page</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">frontmatter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 判斷是否為文章頁面</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> isArticlePage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> computed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (frontmatter.value.date </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> frontmatter.value.tags) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">         !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">page.value.relativePath.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'index.md'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br></div></div><p>這樣做的好處是：</p>
<ul>
<li>留言區只會出現在正式的文章頁面</li>
<li>首頁、分類頁這些列表頁不會有留言區</li>
<li>如果某篇文章不想開放留言，只要在 frontmatter 加上 <code>comment: false</code> 就行</li>
</ul>
<hr>
<h3 id="第五步-樣式調整" tabindex="-1">第五步：樣式調整 <a class="header-anchor" href="#第五步-樣式調整" aria-label="Permalink to “第五步：樣式調整”">&#8203;</a></h3>
<p>Waline 預設的樣式可能跟你的主題不太搭，可以加一些 CSS 調整：</p>
<div class="language-css line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.waline-wrapper</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  margin-top</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">4</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  padding-top</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  border-top</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">px</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> solid</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--vp-c-divider</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/* 深色模式適配 */</span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">html</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.dark</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> .wl-card</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  background-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--vp-c-bg-soft</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--vp-c-text-1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">html</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.dark</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> .wl-editor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  background-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--vp-c-bg-soft</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  border-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--vp-c-divider</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/* 按鈕樣式與主題一致 */</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.wl-btn</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  background-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--vp-c-brand-1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!important</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.wl-btn:hover</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  background-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--vp-c-brand-2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!important</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/* 統一操作按鈕顏色 */</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.wl-login-btn</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.wl-logout-btn</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.wl-refresh</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--vp-c-brand-1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!important</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br></div></div><hr>
<h2 id="後端部署-vercel" tabindex="-1">後端部署（Vercel） <a class="header-anchor" href="#後端部署-vercel" aria-label="Permalink to “後端部署（Vercel）”">&#8203;</a></h2>
<p>前端整合完成後，還需要部署 Waline 的後端服務。</p>
<p>最簡單的方式是用 Vercel：</p>
<h3 id="_1-一鍵部署到-vercel" tabindex="-1">1. 一鍵部署到 Vercel <a class="header-anchor" href="#_1-一鍵部署到-vercel" aria-label="Permalink to “1. 一鍵部署到 Vercel”">&#8203;</a></h3>
<p>點擊以下連結直接部署（會自動建立正確的範本倉庫）：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>https://vercel.com/new/clone?repository-url=https://github.com/walinejs/waline/tree/main/example</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p><strong>重要提醒</strong>：不要直接 fork Waline 主專案再部署，那樣會因為包含完整原始碼而導致構建失敗。使用上面的一鍵部署連結，Vercel 會自動從 <code>example</code> 目錄建立正確的範本。</p>
<h3 id="_2-註冊-leancloud-資料庫" tabindex="-1">2. 註冊 LeanCloud 資料庫 <a class="header-anchor" href="#_2-註冊-leancloud-資料庫" aria-label="Permalink to “2. 註冊 LeanCloud 資料庫”">&#8203;</a></h3>
<p>在 <a href="https://console.leancloud.app/" target="_blank" rel="noreferrer">LeanCloud 國際版</a> 註冊並建立應用，取得以下憑證：</p>
<ol>
<li>進入應用設定 → 應用憑證</li>
<li>複製以下資訊：
<ul>
<li><strong>App ID</strong></li>
<li><strong>App Key</strong></li>
<li><strong>Master Key</strong></li>
<li><strong>伺服器地址（REST API）</strong> - 這就是 LEAN_SERVER</li>
</ul>
</li>
</ol>
<h3 id="_3-設定-vercel-環境變數" tabindex="-1">3. 設定 Vercel 環境變數 <a class="header-anchor" href="#_3-設定-vercel-環境變數" aria-label="Permalink to “3. 設定 Vercel 環境變數”">&#8203;</a></h3>
<p>回到 Vercel 專案，進入 Settings → Environment Variables，新增：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>LEAN_ID=your_leancloud_app_id</span></span>
<span class="line"><span>LEAN_KEY=your_leancloud_app_key</span></span>
<span class="line"><span>LEAN_MASTER_KEY=your_leancloud_master_key</span></span>
<span class="line"><span>LEAN_SERVER=https://your_app_id.api.lncldglobal.com</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p><strong>說明</strong>：</p>
<ul>
<li>前三個從 LeanCloud 「應用憑證」複製</li>
<li><code>LEAN_SERVER</code> 是 LeanCloud 的 API 端點地址，通常格式為 <code>https://前8碼.api.lncldglobal.com</code></li>
<li>也可以使用其他資料庫方案（PostgreSQL、MongoDB 等），詳見 <a href="https://waline.js.org/" target="_blank" rel="noreferrer">Waline 文件</a></li>
</ul>
<p>設定完成後，點擊 Redeploy 重新部署。</p>
<h3 id="_4-更新前端設定" tabindex="-1">4. 更新前端設定 <a class="header-anchor" href="#_4-更新前端設定" aria-label="Permalink to “4. 更新前端設定”">&#8203;</a></h3>
<p>部署完成後，你會得到一個 Vercel URL（例如：<code>https://your-waline.vercel.app</code>），把它填到前面元件的 <code>serverURL</code> 設定裡就完成了。</p>
<hr>
<h2 id="踩過的坑" tabindex="-1">踩過的坑 <a class="header-anchor" href="#踩過的坑" aria-label="Permalink to “踩過的坑”">&#8203;</a></h2>
<p>整合過程中遇到幾個小問題，記錄一下：</p>
<h3 id="_1-深色模式不會自動切換" tabindex="-1">1. 深色模式不會自動切換 <a class="header-anchor" href="#_1-深色模式不會自動切換" aria-label="Permalink to “1. 深色模式不會自動切換”">&#8203;</a></h3>
<p>一開始發現切換深色模式時，Waline 的主題沒有跟著變。後來發現要用 <code>watch</code> 監聽 <code>isDark</code> 變化，然後呼叫 <code>update()</code> 方法更新主題。</p>
<h3 id="_2-路由切換時留言沒更新" tabindex="-1">2. 路由切換時留言沒更新 <a class="header-anchor" href="#_2-路由切換時留言沒更新" aria-label="Permalink to “2. 路由切換時留言沒更新”">&#8203;</a></h3>
<p>在 VitePress 中切換文章時是用戶端路由，Waline 不會自動更新。需要監聽 <code>route.path</code> 變化，手動觸發 <code>update()</code>。</p>
<h3 id="_3-別忘了清理實體" tabindex="-1">3. 別忘了清理實體 <a class="header-anchor" href="#_3-別忘了清理實體" aria-label="Permalink to “3. 別忘了清理實體”">&#8203;</a></h3>
<p>如果沒有在 <code>onUnmounted</code> 清理 Waline 實體，會有記憶體洩漏的風險，特別是在頻繁切換頁面時。</p>
<hr>
<h2 id="進階功能" tabindex="-1">進階功能 <a class="header-anchor" href="#進階功能" aria-label="Permalink to “進階功能”">&#8203;</a></h2>
<p>Waline 還有一些好用的進階功能：</p>
<h3 id="郵件通知-如果有-smtp" tabindex="-1">郵件通知 (如果有 SMTP) <a class="header-anchor" href="#郵件通知-如果有-smtp" aria-label="Permalink to “郵件通知 (如果有 SMTP)”">&#8203;</a></h3>
<p>可以設定當有新留言或被回覆時，自動發送郵件通知。</p>
<p>在 Vercel 環境變數中設定：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>SMTP_SERVICE=Gmail</span></span>
<span class="line"><span>SMTP_USER=your_email@gmail.com</span></span>
<span class="line"><span>SMTP_PASS=your_app_password</span></span>
<span class="line"><span>SITE_NAME=你的部落格名稱</span></span>
<span class="line"><span>SITE_URL=https://yourblog.com</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><h3 id="整合-akismet-反垃圾留言" tabindex="-1">整合 Akismet 反垃圾留言 <a class="header-anchor" href="#整合-akismet-反垃圾留言" aria-label="Permalink to “整合 Akismet 反垃圾留言”">&#8203;</a></h3>
<p>整合 Akismet 來過濾垃圾留言：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>AKISMET_KEY=your_akismet_key</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><h3 id="管理後台" tabindex="-1">管理後台 <a class="header-anchor" href="#管理後台" aria-label="Permalink to “管理後台”">&#8203;</a></h3>
<p>瀏覽 <code>https://your-waline-server.vercel.app/ui</code> 可以進入管理後台，審核和管理留言。</p>
<hr>
<h2 id="總結" tabindex="-1">總結 <a class="header-anchor" href="#總結" aria-label="Permalink to “總結”">&#8203;</a></h2>
<p>整合 Waline 到 VitePress 其實沒想像中複雜，主要就是：</p>
<ol>
<li>安裝套件</li>
<li>建立 Vue 元件</li>
<li>處理深色模式和路由更新</li>
<li>部署後端服務</li>
<li>調整樣式</li>
</ol>
<p>使用 Waline 後，留言系統變得輕量又好用，對技術部落格來說非常合適。
部署在 Vercel 也省去了維護後端的麻煩，跟部落格用同一個平台，感覺很一致。
包括你正在看的這篇文章底下的留言區，就是用 Waline 實作的！</p>
<p>如果你也在找適合技術部落格的留言系統，Waline 真的是個不錯的選擇。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vitepress</category>
            <category>waline</category>
            <category>vue</category>
        </item>
        <item>
            <title><![CDATA[VitePress 自動生成 Open Graph 圖片]]></title>
            <link>https://kurohsu.dev/notes/vitepress-自動生成-og-圖片.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/vitepress-自動生成-og-圖片.html</guid>
            <pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="vitepress-自動生成-open-graph-圖片" tabindex="-1">VitePress 自動生成 Open Graph 圖片 <a class="header-anchor" href="#vitepress-自動生成-open-graph-圖片" aria-label="Permalink to “VitePress 自動生成 Open Graph 圖片”">&#8203;</a></h1>
<p>把部落格架好之後，開始在意分享到社群媒體時的呈現效果。</p>
<p>你知道那種在 Facebook、Twitter 或 Discord 上貼連結時，會自動出現一張漂亮的預覽圖嗎？那就是 Open Graph (OG) 圖片。
這篇文章要來聊聊如何在 VitePress 中自動生成這些圖片，省去手動製作的麻煩。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="vitepress-自動生成-open-graph-圖片" tabindex="-1">VitePress 自動生成 Open Graph 圖片 <a class="header-anchor" href="#vitepress-自動生成-open-graph-圖片" aria-label="Permalink to “VitePress 自動生成 Open Graph 圖片”">&#8203;</a></h1>
<p>把部落格架好之後，開始在意分享到社群媒體時的呈現效果。</p>
<p>你知道那種在 Facebook、Twitter 或 Discord 上貼連結時，會自動出現一張漂亮的預覽圖嗎？那就是 Open Graph (OG) 圖片。
這篇文章要來聊聊如何在 VitePress 中自動生成這些圖片，省去手動製作的麻煩。</p>
<hr>
<h2 id="什麼是-open-graph-圖片" tabindex="-1">什麼是 Open Graph 圖片？ <a class="header-anchor" href="#什麼是-open-graph-圖片" aria-label="Permalink to “什麼是 Open Graph 圖片？”">&#8203;</a></h2>
<p>Open Graph (OG) 是 Facebook 在 2010 年推出的協定，讓網頁可以更好地被社群媒體平台解析和展示。
當你在社群平台分享連結時，平台會讀取網頁 <code>&lt;head&gt;</code> 中的 OG meta 標籤，抓取標題、描述和圖片來產生預覽卡片。</p>
<p>最常見的 OG 標籤：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">meta</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> property</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"og:title"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"文章標題"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">meta</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> property</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"og:description"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"文章摘要"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">meta</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> property</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"og:image"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://example.com/og-image.png"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">meta</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> property</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"og:image:width"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"1200"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">meta</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> property</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"og:image:height"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"630"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p><strong>標準尺寸</strong>：1200×630 像素（這是 Facebook、Twitter 建議的比例）</p>
<hr>
<h2 id="為什麼要自動生成" tabindex="-1">為什麼要自動生成？ <a class="header-anchor" href="#為什麼要自動生成" aria-label="Permalink to “為什麼要自動生成？”">&#8203;</a></h2>
<p>一開始我也想過用 Figma 或 Canva 手動做圖，但馬上發現問題：</p>
<ol>
<li><strong>太費時間</strong>：部落格有幾十篇文章，每篇都要開 Figma 改標題、匯出圖片，太累了</li>
<li><strong>不好維護</strong>：如果文章標題改了，圖片也要重做</li>
<li><strong>風格不一致</strong>：手動做圖難免有誤差，自動生成可以確保所有圖片風格統一</li>
<li><strong>忘記做圖</strong>：寫新文章時很容易忘記做 OG 圖片</li>
</ol>
<p>因為開發者就是懶，所以最好的方式就是，在 Blog 建置時自動生成。
而 VitePress 本身就有提供擴充功能的機制，可以在建置過程中插入自定義邏輯，這正好派上用場。</p>
<hr>
<h2 id="實作架構" tabindex="-1">實作架構 <a class="header-anchor" href="#實作架構" aria-label="Permalink to “實作架構”">&#8203;</a></h2>
<p>既然決定好了要自動生成 OG 圖片，接下來就是設計整個系統的架構，整個系統分成三個部分：</p>
<ol>
<li><strong>SVG 模板</strong>：定義圖片的設計和排版</li>
<li><strong>生成腳本</strong>：讀取模板、替換文字、轉換成 PNG</li>
<li><strong>VitePress 整合</strong>：在建置時自動執行，生成所有頁面的 OG 圖片</li>
</ol>
<p>技術選擇：</p>
<ul>
<li><strong>SVG</strong> 作為模板，向量圖好編輯，也方便程式化</li>
<li><strong>Sharp</strong> 轉換 SVG 為高品質 PNG（Node.js 最快的圖片處理 lib）</li>
<li><strong>VitePress <code>transformPageData</code> hook</strong>：在建置時同步處理每個頁面，生成對應的 OG 圖片</li>
</ul>
<hr>
<h2 id="第一步-安裝-sharp" tabindex="-1">第一步：安裝 Sharp <a class="header-anchor" href="#第一步-安裝-sharp" aria-label="Permalink to “第一步：安裝 Sharp”">&#8203;</a></h2>
<p>Sharp 是一個高效能的 Node.js 圖片處理 lib，用來把 SVG 轉換成 PNG。</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -D</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> sharp</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>如果你用 npm 或 yarn：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --save-dev</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> sharp</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># or</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">yarn</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -D</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> sharp</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><hr>
<h2 id="第二步-設計-svg-模板" tabindex="-1">第二步：設計 SVG 模板 <a class="header-anchor" href="#第二步-設計-svg-模板" aria-label="Permalink to “第二步：設計 SVG 模板”">&#8203;</a></h2>
<p>在專案根目錄建立 <code>scripts/og-template.svg</code>。</p>
<p>這裡要注意幾件事：</p>
<ol>
<li><strong>使用 2 倍尺寸</strong>：SVG 設計成 2400×1260，最後縮放成 1200×630</li>
<li><strong>為什麼用 2 倍？</strong> 確保文字清晰，避免縮放後模糊</li>
<li><strong>使用 <pre class="inline" v-pre>{{title}}</pre> 佔位符</strong>：稍後用程式替換成真正的標題</li>
</ol>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">svg</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> width</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"2400"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> height</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"1260"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> viewBox</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0 0 2400 1260"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> xmlns</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"http://www.w3.org/2000/svg"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  &#x3C;!-- 背景漸層 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">defs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">linearGradient</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"bgGradient"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> x1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> y1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> x2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"100%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> y2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"100%"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">stop</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> offset</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"stop-color:#1e3a8a;stop-opacity:1"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">stop</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> offset</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"100%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"stop-color:#3b82f6;stop-opacity:1"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">linearGradient</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">defs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  &#x3C;!-- 背景 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">rect</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> width</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"2400"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> height</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"1260"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"url(#bgGradient)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">/></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  &#x3C;!-- 內容區 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">g</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> transform</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"translate(160, 0)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    &#x3C;!-- 文章標題（這行會被腳本替換） --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">text</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> x</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> y</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"500"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> font-family</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"PingFang TC, Microsoft JhengHei, Noto Sans TC, Heiti TC, sans-serif"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> font-size</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"144"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> font-weight</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"500"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#ffffff"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> text-anchor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"start"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      {{title}}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    &#x3C;!-- 網站名稱 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">text</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> x</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> y</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"960"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> font-family</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"PingFang TC, Microsoft JhengHei, Noto Sans TC, Heiti TC, sans-serif"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> font-size</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"72"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> font-weight</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"400"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#e0e7ff"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> text-anchor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"start"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      Kuro Hsu 的筆記</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    &#x3C;!-- 網域 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">text</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> x</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> y</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"1080"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> font-family</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"PingFang TC, Microsoft JhengHei, Noto Sans TC, Heiti TC, sans-serif"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> font-size</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"56"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> font-weight</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"400"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#c7d2fe"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> text-anchor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"start"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      kurohsu.dev</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">svg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br></div></div><p><strong>設計說明</strong>：</p>
<ul>
<li><strong>藍色漸層背景</strong>：從深藍 <code>#1e3a8a</code> 到亮藍 <code>#3b82f6</code>，跟部落格主色調一致</li>
<li><strong>繁體中文字型</strong>：<code>PingFang TC, Microsoft JhengHei, Noto Sans TC</code> 確保中文顯示正常</li>
<li><strong>留白設計</strong>：<code>translate(160, 0)</code> 把內容往右移，左側留白比較舒服</li>
<li><strong>層次分明</strong>：標題用白色、網站名稱和網域用淺色，形成視覺層次</li>
</ul>
<hr>
<h2 id="第三步-撰寫生成腳本" tabindex="-1">第三步：撰寫生成腳本 <a class="header-anchor" href="#第三步-撰寫生成腳本" aria-label="Permalink to “第三步：撰寫生成腳本”">&#8203;</a></h2>
<p>建立 <code>scripts/generateOgImage.ts</code>。</p>
<p>這個腳本的核心功能：</p>
<ol>
<li>讀取 SVG 模板</li>
<li>處理長標題（自動斷行）</li>
<li>跳脫 XML 特殊字元</li>
<li>用 Sharp 轉換成高品質 PNG</li>
</ol>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> fs </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'fs'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'path'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> sharp </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'sharp'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { fileURLToPath } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'url'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> __dirname</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">dirname</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">fileURLToPath</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">meta</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.url))</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * 將長文字分成多行</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@param</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> text</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> 要分行的文字</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@param</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> maxCharsPerLine</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> 每行最大字元數</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@param</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> maxLines</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> 最大行數</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> splitTextIntoLines</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">text</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">maxCharsPerLine</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">maxLines</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[] {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> lines</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> currentLine </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 處理中文和英文混合的文字</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> char</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> of</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> text) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (currentLine.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> >=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> maxCharsPerLine) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      lines.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">push</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(currentLine)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      currentLine </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> char</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (lines.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> >=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> maxLines </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        break</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      currentLine </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> char</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (currentLine </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> lines.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x3C;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> maxLines) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    lines.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">push</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(currentLine)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 如果最後一行太長，加上省略號</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (text.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> currentLine.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> lines.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    lines[lines.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> lines[lines.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">slice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '...'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> lines</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * 跳脫 XML 特殊字元</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> escapeXml</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">text</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> text</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">&#x26;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;amp;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">&#x3C;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;lt;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">></span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;gt;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;quot;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">'</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;apos;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * 生成 OG 圖片</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@param</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> title</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> 文章標題</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@param</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> outputPath</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> 輸出路徑</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> generateOgImage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">title</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">outputPath</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">void</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 確保輸出目錄存在</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> outputDir</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">dirname</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outputPath)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">existsSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outputDir)) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">mkdirSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outputDir, { recursive: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 讀取 SVG 模板</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> templatePath</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(__dirname, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'og-template.svg'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> svgTemplate </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> fs.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">readFileSync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(templatePath, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'utf-8'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 將標題分成多行（每行最多 20 個字元，最多 3 行）</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> lines</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> splitTextIntoLines</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(title, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 建構多行文字的 SVG (使用 2x 解析度)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> textElements </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> startY</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 400</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 2x of 200</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> lineHeight</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 160</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 2x of 80</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    lines.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">forEach</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">line</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">index</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> y</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> startY </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (index </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> lineHeight)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      textElements </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      &#x3C;text x="0" y="${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">y</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}" font-family="PingFang TC, Microsoft JhengHei, Noto Sans TC, Heiti TC, sans-serif" font-size="144" font-weight="500" fill="#ffffff" text-anchor="start"></span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        ${</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">escapeXml</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">(</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">line</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">)</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      &#x3C;/text>`</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 替換模板中的標題</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    svgTemplate </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> svgTemplate.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      /</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">&#x3C;text</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">[</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">^</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">>]</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">></span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\s</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\{\{</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">title</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\}\}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\s</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">text></span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">s</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      textElements</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    )</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 將 SVG (2400x1260) 轉換並縮放為 PNG (1200x630)</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 使用高密度渲染和 Lanczos3 縮放確保文字清晰</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sharp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Buffer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">from</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(svgTemplate), {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      density: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">200</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 提高 SVG 渲染密度到 200 DPI</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">resize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1200</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">630</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        fit: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'cover'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        kernel: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'lanczos3'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 使用高品質的 Lanczos3 縮放演算法</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        position: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'center'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">png</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        compressionLevel: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">6</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// PNG 壓縮等級 (0-9)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        palette: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 使用全彩 PNG</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toFile</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outputPath)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`✓ Generated OG image: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">outputPath</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`✗ Failed to generate OG image for "${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">title</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}":`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    throw</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> error</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br><span class="line-number">55</span><br><span class="line-number">56</span><br><span class="line-number">57</span><br><span class="line-number">58</span><br><span class="line-number">59</span><br><span class="line-number">60</span><br><span class="line-number">61</span><br><span class="line-number">62</span><br><span class="line-number">63</span><br><span class="line-number">64</span><br><span class="line-number">65</span><br><span class="line-number">66</span><br><span class="line-number">67</span><br><span class="line-number">68</span><br><span class="line-number">69</span><br><span class="line-number">70</span><br><span class="line-number">71</span><br><span class="line-number">72</span><br><span class="line-number">73</span><br><span class="line-number">74</span><br><span class="line-number">75</span><br><span class="line-number">76</span><br><span class="line-number">77</span><br><span class="line-number">78</span><br><span class="line-number">79</span><br><span class="line-number">80</span><br><span class="line-number">81</span><br><span class="line-number">82</span><br><span class="line-number">83</span><br><span class="line-number">84</span><br><span class="line-number">85</span><br><span class="line-number">86</span><br><span class="line-number">87</span><br><span class="line-number">88</span><br><span class="line-number">89</span><br><span class="line-number">90</span><br><span class="line-number">91</span><br><span class="line-number">92</span><br><span class="line-number">93</span><br><span class="line-number">94</span><br><span class="line-number">95</span><br><span class="line-number">96</span><br><span class="line-number">97</span><br><span class="line-number">98</span><br><span class="line-number">99</span><br><span class="line-number">100</span><br><span class="line-number">101</span><br><span class="line-number">102</span><br><span class="line-number">103</span><br><span class="line-number">104</span><br><span class="line-number">105</span><br><span class="line-number">106</span><br><span class="line-number">107</span><br><span class="line-number">108</span><br><span class="line-number">109</span><br><span class="line-number">110</span><br><span class="line-number">111</span><br><span class="line-number">112</span><br><span class="line-number">113</span><br><span class="line-number">114</span><br><span class="line-number">115</span><br><span class="line-number">116</span><br></div></div><p><strong>關鍵技術細節</strong>：</p>
<ol>
<li><strong>自動斷行</strong>：中英文混合時，每 20 個字元斷行，最多 3 行</li>
<li><strong>超長標題處理</strong>：第 3 行末尾加 <code>...</code> 省略號</li>
<li><strong>XML Escape</strong>：標題中的 <code>&amp;</code>, <code>&lt;</code>, <code>&gt;</code> 等特殊字元要 Encode，不然 SVG 會壞掉</li>
<li><strong>高品質轉換</strong>：
<ul>
<li><code>density: 200</code> 提高 SVG 渲染密度（預設是 72 DPI）</li>
<li><code>kernel: 'lanczos3'</code> 使用最高品質的縮放演算法</li>
<li>先生成 2 倍圖再縮小，確保文字銳利</li>
</ul>
</li>
</ol>
<hr>
<h2 id="第四步-整合到-vitepress" tabindex="-1">第四步：整合到 VitePress <a class="header-anchor" href="#第四步-整合到-vitepress" aria-label="Permalink to “第四步：整合到 VitePress”">&#8203;</a></h2>
<p>修改 <code>docs/.vitepress/config.ts</code>，使用 VitePress 的 <code>transformPageData</code> hook：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { defineConfig } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vitepress'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { generateOgImage } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '../../scripts/generateOgImage'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'path'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> siteUrl</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'https://kurohsu.dev'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> buildTimestamp</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  async</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> transformPageData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">pageData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 為動態標籤頁面設定正確的標題</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (pageData.relativePath.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">startsWith</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'tags/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        pageData.relativePath </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'tags/index.md'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        pageData.params?.tag) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      pageData.title </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `標籤：${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">pageData</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">params</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">tag</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 為有標題的頁面生成 OG 圖片（排除首頁）</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (pageData.title </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pageData.title </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'Kuro Hsu 的筆記'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ogImagePath</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `/og/${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">pageData</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">relativePath</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">md</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.png'</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">)</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> outputPath</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(process.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">cwd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(), </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'docs/public'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, ogImagePath)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 生成 OG 圖片</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> generateOgImage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(pageData.title, outputPath)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 初始化 head 陣列</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">pageData.frontmatter.head) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          pageData.frontmatter.head </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 新增時間戳 query string 來防止快取</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ogImageUrl</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">ogImagePath</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}?v=${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">buildTimestamp</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 注入 OG meta 標籤</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        pageData.frontmatter.head.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">push</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'meta'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { property: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'og:image'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: ogImageUrl }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'meta'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { property: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'og:image:width'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'1200'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'meta'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { property: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'og:image:height'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'630'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'meta'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { property: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'og:title'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: pageData.title }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'meta'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { property: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'og:description'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: pageData.description </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pageData.frontmatter.description </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '技術筆記、學習紀錄與生活記錄'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'meta'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { property: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'twitter:card'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'summary_large_image'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'meta'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { property: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'twitter:image'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: ogImageUrl }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'meta'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { property: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'twitter:title'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: pageData.title }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'meta'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { property: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'twitter:description'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: pageData.description </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pageData.frontmatter.description </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '技術筆記、學習紀錄與生活記錄'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        )</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">warn</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`Warning: Failed to generate OG image for ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">pageData</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">relativePath</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}:`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ... 其他設定</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br></div></div><p><strong>這段程式碼做了什麼？</strong></p>
<ol>
<li><strong>在建置時執行</strong>：VitePress 建置每個頁面時都會呼叫 <code>transformPageData</code></li>
<li><strong>動態標籤頁處理</strong>：標籤頁面沒有預設標題，要手動設定</li>
<li><strong>生成 OG 圖片</strong>：呼叫剛才寫的 <code>generateOgImage()</code> 函式</li>
<li><strong>輸出路徑對應頁面結構</strong>：
<ul>
<li><code>docs/notes/my-article.md</code> → <code>docs/public/og/notes/my-article.png</code></li>
</ul>
</li>
<li><strong>自動注入 meta 標籤</strong>：把 OG 和 Twitter Card 標籤加到頁面 <code>&lt;head&gt;</code> 裡</li>
<li><strong>Cache busting</strong>：URL 加上時間戳 <code>?v={buildTimestamp}</code>，確保分享時看到最新圖片</li>
<li><strong>錯誤處理</strong>：如果某個圖片生成失敗，只顯示警告，不會中斷建置</li>
</ol>
<hr>
<h2 id="第五步-設定-gitignore" tabindex="-1">第五步：設定 .gitignore <a class="header-anchor" href="#第五步-設定-gitignore" aria-label="Permalink to “第五步：設定 .gitignore”">&#8203;</a></h2>
<p>生成的 OG 圖片不需要加入版控，因為每次建置都會重新生成。</p>
<p>在 <code>.gitignore</code> 加上：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span># OG 圖片（建置時自動生成）</span></span>
<span class="line"><span>docs/public/og/</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p>這樣做的好處：</p>
<ul>
<li>減少 Git Repo 大小</li>
<li>避免圖片衝突</li>
<li>確保 OG 圖片永遠是最新的</li>
</ul>
<hr>
<h2 id="測試一下" tabindex="-1">測試一下 <a class="header-anchor" href="#測試一下" aria-label="Permalink to “測試一下”">&#8203;</a></h2>
<p>現在執行建置：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> build</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>你應該會看到終端機輸出類似這樣的訊息：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>✓ Generated OG image: /path/to/docs/public/og/notes/my-article.png</span></span>
<span class="line"><span>✓ Generated OG image: /path/to/docs/public/og/learn/another-article.png</span></span>
<span class="line"><span>...</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>檢查一下生成的圖片：</p>
<div class="language-bash line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ls</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> docs/public/og/notes/</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>應該會看到每篇文章都有對應的 PNG 圖片！</p>
<hr>
<h2 id="驗證-og-圖片是否正確" tabindex="-1">驗證 OG 圖片是否正確 <a class="header-anchor" href="#驗證-og-圖片是否正確" aria-label="Permalink to “驗證 OG 圖片是否正確”">&#8203;</a></h2>
<p>部署上線後，可以用以下工具測試：</p>
<ol>
<li>
<p><strong>Facebook Sharing Debugger</strong></p>
<ul>
<li><a href="https://developers.facebook.com/tools/debug/" target="_blank" rel="noreferrer">https://developers.facebook.com/tools/debug/</a></li>
<li>貼上文章 URL，看看 Facebook 抓到什麼</li>
</ul>
</li>
<li>
<p><strong>Twitter Card Validator</strong></p>
<ul>
<li><a href="https://cards-dev.twitter.com/validator" target="_blank" rel="noreferrer">https://cards-dev.twitter.com/validator</a></li>
<li>檢查 Twitter 卡片顯示</li>
</ul>
</li>
<li>
<p><strong>Discord</strong></p>
<ul>
<li>直接在 Discord 貼上連結，看看預覽是否正確</li>
</ul>
</li>
</ol>
<p><strong>第一次測試記得清快取</strong>：這些平台都會快取 OG 圖片，如果改了設定但看不到變化，用上面的工具「重新抓取」。</p>
<hr>
<h2 id="踩過的坑" tabindex="-1">踩過的坑 <a class="header-anchor" href="#踩過的坑" aria-label="Permalink to “踩過的坑”">&#8203;</a></h2>
<p>整合過程中遇到幾個問題，記錄一下：</p>
<h3 id="_1-中文字型不顯示" tabindex="-1">1. 中文字型不顯示 <a class="header-anchor" href="#_1-中文字型不顯示" aria-label="Permalink to “1. 中文字型不顯示”">&#8203;</a></h3>
<p>一開始只設定 <code>font-family: &quot;sans-serif&quot;</code>，結果中文變成方塊字。</p>
<p><strong>解決方法</strong>：在 SVG 中明確指定繁體中文字型：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>font-family="PingFang TC, Microsoft JhengHei, Noto Sans TC, Heiti TC, sans-serif"</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>這些是 macOS 和 Windows 常見的繁中字型，Sharp 會依序嘗試。</p>
<h3 id="_2-文字模糊" tabindex="-1">2. 文字模糊 <a class="header-anchor" href="#_2-文字模糊" aria-label="Permalink to “2. 文字模糊”">&#8203;</a></h3>
<p>最初直接用 1200×630 的 SVG，轉 PNG 後文字有點糊。</p>
<p><strong>解決方法</strong>：SVG 用 2 倍尺寸（2400×1260），轉 PNG 時縮小：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sharp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Buffer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">from</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(svgTemplate), {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  density: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">200</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 高密度渲染</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">resize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1200</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">630</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    kernel: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'lanczos3'</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 高品質縮放</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>這樣文字就清晰很多。</p>
<h3 id="_3-標題太長跑出畫面" tabindex="-1">3. 標題太長跑出畫面 <a class="header-anchor" href="#_3-標題太長跑出畫面" aria-label="Permalink to “3. 標題太長跑出畫面”">&#8203;</a></h3>
<p>有些文章標題很長，例如「透過 Composition Events 增強非拉丁語系輸入法對輸入框的支援」。
不處理的話文字會跑出 SVG 畫布。</p>
<p><strong>解決方法</strong>：寫了 <code>splitTextIntoLines()</code> 函式：</p>
<ul>
<li>每行最多 20 個字元</li>
<li>最多 3 行</li>
<li>超過的話第 3 行加 <code>...</code></li>
</ul>
<h3 id="_4-特殊字元炸掉-svg" tabindex="-1">4. 特殊字元炸掉 SVG <a class="header-anchor" href="#_4-特殊字元炸掉-svg" aria-label="Permalink to “4. 特殊字元炸掉 SVG”">&#8203;</a></h3>
<p>文章標題如果有 <code>&lt;</code>, <code>&gt;</code>, <code>&amp;</code> 這些字元，會讓 SVG 解析失敗。</p>
<p><strong>解決方法</strong>：用 <code>escapeXml()</code> 跳脫：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> escapeXml</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">text</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> text</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">&#x26;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;amp;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">&#x3C;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;lt;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">></span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;gt;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;quot;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">'</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x26;apos;'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><h3 id="_5-建置時間變長" tabindex="-1">5. 建置時間變長 <a class="header-anchor" href="#_5-建置時間變長" aria-label="Permalink to “5. 建置時間變長”">&#8203;</a></h3>
<p>每個頁面都生成圖片會拉長建置時間。</p>
<p>我的部落格有 50+ 篇文章，建置時間從 10 秒增加到 20 秒，雖然慢一點，但省下手動做圖的時間絕對值得。
而且只有建置時才會跑，開發模式時不受影響。</p>
<h3 id="_6-vercel-建置失敗" tabindex="-1">6. Vercel 建置失敗 <a class="header-anchor" href="#_6-vercel-建置失敗" aria-label="Permalink to “6. Vercel 建置失敗”">&#8203;</a></h3>
<p>部署到 Vercel 時出現 <code>sharp</code> 安裝失敗的錯誤。</p>
<p><strong>解決方法</strong>：在 <code>package.json</code> 加上：</p>
<div class="language-json line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "pnpm"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    "onlyBuiltDependencies"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      "sharp"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><p>告訴 pnpm 在 Vercel 環境要重新編譯 Sharp（因為它是 native module，需要針對部署環境編譯）。</p>
<h3 id="_7-facebook-無法顯示中文檔名的-og-圖片" tabindex="-1">7. Facebook 無法顯示中文檔名的 OG 圖片 <a class="header-anchor" href="#_7-facebook-無法顯示中文檔名的-og-圖片" aria-label="Permalink to “7. Facebook 無法顯示中文檔名的 OG 圖片”">&#8203;</a></h3>
<p>文章檔名使用中文時，Facebook Sharing Debugger 會顯示「圖像損毀」錯誤。</p>
<p><strong>問題原因</strong>：OG image URL 包含未編碼的中文字元，社群媒體平台無法正確解析。</p>
<p>例如原本的 URL：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>https://kurohsu.dev/og/notes/vitepress-自動生成-og-圖片.png</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>Facebook 無法讀取這個 URL，需要進行 URL 編碼。</p>
<p><strong>解決方法</strong>：在 <code>config.ts</code> 中對檔名進行 URL 編碼：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 對 URL 進行編碼，確保中文檔名可以被正確處理</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 將路徑分解為目錄和檔名，只對檔名部分進行編碼</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> pathParts</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ogImagePath.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">split</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> encodedPath</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pathParts.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">part</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">index</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  index </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pathParts.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> encodeURIComponent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(part) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> part</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 添加時間戳 query string 來防止快取</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ogImageUrl</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">siteUrl</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">encodedPath</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}?v=${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">buildTimestamp</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><p>編碼後的 URL：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>https://kurohsu.dev/og/notes/vitepress-%E8%87%AA%E5%8B%95%E7%94%9F%E6%88%90-og-%E5%9C%96%E7%89%87.png</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>這樣 Facebook、Twitter、Discord 等平台就能正確抓取圖片了！</p>
<p><strong>測試步驟</strong>：</p>
<ol>
<li>部署後前往 <a href="https://developers.facebook.com/tools/debug/" target="_blank" rel="noreferrer">Facebook Sharing Debugger</a></li>
<li>貼上文章 URL 並點擊「重新抓取」</li>
<li>確認圖片正確顯示</li>
</ol>
<hr>
<h2 id="進階-自訂樣式" tabindex="-1">進階：自訂樣式 <a class="header-anchor" href="#進階-自訂樣式" aria-label="Permalink to “進階：自訂樣式”">&#8203;</a></h2>
<p>如果你想改變 OG 圖片的設計，只要修改 <code>scripts/og-template.svg</code>。</p>
<h3 id="範例-加上分類標籤" tabindex="-1">範例：加上分類標籤 <a class="header-anchor" href="#範例-加上分類標籤" aria-label="Permalink to “範例：加上分類標籤”">&#8203;</a></h3>
<p>可以在標題旁邊顯示文章分類（notes / learn / misc）：</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 在 generateOgImage() 函式中</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getCategory</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">path</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/notes/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '程式碼筆記'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/learn/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '學習紀錄'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/misc/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '雜記'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 修改 SVG，加上分類文字</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> category</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getCategory</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(outputPath)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (category) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  textElements </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> `</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    &#x3C;text x="0" y="320" font-size="60" fill="#93c5fd">${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">category</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}&#x3C;/text></span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  `</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> textElements</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br></div></div><h3 id="範例-換成深色背景" tabindex="-1">範例：換成深色背景 <a class="header-anchor" href="#範例-換成深色背景" aria-label="Permalink to “範例：換成深色背景”">&#8203;</a></h3>
<p>改 SVG 的漸層顏色：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">linearGradient</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"bgGradient"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> x1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> y1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> x2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"100%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> y2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"100%"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">stop</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> offset</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"0%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"stop-color:#1f2937;stop-opacity:1"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">stop</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> offset</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"100%"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"stop-color:#111827;stop-opacity:1"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">linearGradient</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p>改完之後重新建置，所有 OG 圖片就會套用新設計！</p>
<hr>
<h2 id="總結" tabindex="-1">總結 <a class="header-anchor" href="#總結" aria-label="Permalink to “總結”">&#8203;</a></h2>
<p>自動生成 OG 圖片的好處：</p>
<ol>
<li><strong>省時間</strong>：不用為每篇文章手動做圖</li>
<li><strong>一致性</strong>：所有文章的 OG 圖片風格統一</li>
<li><strong>可維護</strong>：標題改了，重新建置就好，圖片會自動更新</li>
<li><strong>SEO 友善</strong>：分享到社群媒體時有漂亮的預覽卡片，提高點擊率</li>
</ol>
<p>實作重點：</p>
<ol>
<li>SVG 作為模板（好編輯、向量圖清晰）</li>
<li>Sharp 轉換成 PNG（高效能、高品質）</li>
<li>VitePress <code>transformPageData</code> hook（建置時自動執行）</li>
<li>處理長標題、特殊字元、中文字型</li>
</ol>
<p>整合完成後，寫新文章只要專心寫內容，OG 圖片會自動生成，部落格的分享體驗立刻升級！</p>
<p>最終在 Facebook Sharing Debugger 測試的成果會像這樣：
<img src="/images/notes/vitepress-og-image-facebook-debugger.png" alt="自動生成的 OG 圖, 在 Facebook Sharing Debugger 測試的畫面"></p>
<p>現在你可以把文章分享到 Discord、Twitter 或 Facebook，看看那張自動生成的 OG 預覽圖，是不是很有成就感？</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vitepress</category>
            <category>og-image</category>
            <category>seo</category>
        </item>
        <item>
            <title><![CDATA[JavaScript 整潔程式碼實踐：提升程式碼品質的具體方法]]></title>
            <link>https://kurohsu.dev/notes/javascript-clean-code-practices.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/javascript-clean-code-practices.html</guid>
            <pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="javascript-整潔程式碼實踐-提升程式碼品質的具體方法" tabindex="-1">JavaScript 整潔程式碼實踐：提升程式碼品質的具體方法 <a class="header-anchor" href="#javascript-整潔程式碼實踐-提升程式碼品質的具體方法" aria-label="Permalink to “JavaScript 整潔程式碼實踐：提升程式碼品質的具體方法”">&#8203;</a></h1>
<p>程式碼品質不只是「能跑就好」這麼簡單，更關乎可讀性、可維護性和團隊協作效率。這篇文章整理了我在 JavaScript 開發中實踐整潔程式碼的一些方法，都是踩過坑後的真實心得。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="javascript-整潔程式碼實踐-提升程式碼品質的具體方法" tabindex="-1">JavaScript 整潔程式碼實踐：提升程式碼品質的具體方法 <a class="header-anchor" href="#javascript-整潔程式碼實踐-提升程式碼品質的具體方法" aria-label="Permalink to “JavaScript 整潔程式碼實踐：提升程式碼品質的具體方法”">&#8203;</a></h1>
<p>程式碼品質不只是「能跑就好」這麼簡單，更關乎可讀性、可維護性和團隊協作效率。這篇文章整理了我在 JavaScript 開發中實踐整潔程式碼的一些方法，都是踩過坑後的真實心得。</p>
<hr>
<h2 id="命名的藝術" tabindex="-1">命名的藝術 <a class="header-anchor" href="#命名的藝術" aria-label="Permalink to “命名的藝術”">&#8203;</a></h2>
<h3 id="有意義的命名" tabindex="-1">有意義的命名 <a class="header-anchor" href="#有意義的命名" aria-label="Permalink to “有意義的命名”">&#8203;</a></h3>
<p>命名是程式設計中最重要卻最容易被忽視的環節。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 不好的命名：無法理解變數用途</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> d</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> arr</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> flag </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 好的命名：清楚表達意圖</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> createdAt</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> activeUsers</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> isAuthenticated </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><h3 id="命名的一致性" tabindex="-1">命名的一致性 <a class="header-anchor" href="#命名的一致性" aria-label="Permalink to “命名的一致性”">&#8203;</a></h3>
<p>在專案中保持命名風格的一致性：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 不一致的命名風格</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getUserInfo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">fetchUserData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">retrieveUserDetails</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 一致的命名風格</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getUserProfile</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getUserSettings</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><h3 id="避免魔術數字" tabindex="-1">避免魔術數字 <a class="header-anchor" href="#避免魔術數字" aria-label="Permalink to “避免魔術數字”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 魔術數字讓人困惑</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setTimeout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // do something</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">86400000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 使用有意義的常數</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ONE_DAY_IN_MS</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 24</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 60</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 60</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setTimeout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // do something</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">ONE_DAY_IN_MS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><h2 id="函式設計原則" tabindex="-1">函式設計原則 <a class="header-anchor" href="#函式設計原則" aria-label="Permalink to “函式設計原則”">&#8203;</a></h2>
<h3 id="單一職責原則" tabindex="-1">單一職責原則 <a class="header-anchor" href="#單一職責原則" aria-label="Permalink to “單一職責原則”">&#8203;</a></h3>
<p>一個函式應該只做一件事，並且做好它。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 函式職責過多</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> processUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 驗證使用者</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.email </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.name) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    throw</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Invalid user'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 格式化資料</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> formattedUser</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    ...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    name: user.name.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    email: user.email.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toLowerCase</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 儲存到資料庫</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  database.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">save</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(formattedUser)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 發送通知</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  emailService.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">send</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(user.email, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Welcome!'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> formattedUser</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 拆分成多個職責單一的函式</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> validateUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.email </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.name) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    throw</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Invalid user'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> formatUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    ...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    name: user.name.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    email: user.email.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toLowerCase</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> saveUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> database.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">save</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(user)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sendWelcomeEmail</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> emailService.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">send</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(email, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Welcome!'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 在需要時組合這些函式</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> registerUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  validateUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(user)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> formattedUser</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> formatUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(user)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> saveUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(formattedUser)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sendWelcomeEmail</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(formattedUser.email)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> formattedUser</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br></div></div><h3 id="函式參數的最佳實踐" tabindex="-1">函式參數的最佳實踐 <a class="header-anchor" href="#函式參數的最佳實踐" aria-label="Permalink to “函式參數的最佳實踐”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 參數過多且順序容易混淆</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">age</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">country</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">city</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">zipCode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">phone</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 使用物件參數</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">age</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">address</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">phone</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">country</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">city</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">zipCode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> address</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 呼叫時更清楚</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'John'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  email: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'john@example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  age: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">30</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  address: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    country: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Taiwan'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    city: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Taipei'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    zipCode: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'100'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  phone: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'0912345678'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br></div></div><h3 id="預設參數與解構" tabindex="-1">預設參數與解構 <a class="header-anchor" href="#預設參數與解構" aria-label="Permalink to “預設參數與解構”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 使用預設參數</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  url</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  method</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'GET'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  timeout</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 5000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">  headers</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 解構時給予預設值</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> processConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">config</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {}) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    debug</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    retries</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    cache</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> config</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br></div></div><h2 id="處理非同步程式碼" tabindex="-1">處理非同步程式碼 <a class="header-anchor" href="#處理非同步程式碼" aria-label="Permalink to “處理非同步程式碼”">&#8203;</a></h2>
<h3 id="從-callback-到-async-await" tabindex="-1">從 Callback 到 Async/Await <a class="header-anchor" href="#從-callback-到-async-await" aria-label="Permalink to “從 Callback 到 Async/Await”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ Callback hell</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(userId, (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    handleError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(error)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  getOrders</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(user.id, (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">orders</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      handleError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(error)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    processOrders</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(orders, (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        handleError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(error)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(result)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 使用 async/await</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> processUserOrders</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">userId</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(userId)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> orders</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getOrders</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(user.id)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> processOrders</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(orders)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(result)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    handleError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(error)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br></div></div><h3 id="錯誤處理的最佳實踐" tabindex="-1">錯誤處理的最佳實踐 <a class="header-anchor" href="#錯誤處理的最佳實踐" aria-label="Permalink to “錯誤處理的最佳實踐”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 吞掉錯誤</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> data</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> api.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/data'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> data</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 靜默失敗</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 適當的錯誤處理</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> data</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> api.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/data'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { success: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, data }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Failed to fetch data:'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error)</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 根據錯誤類型決定是否重試或回傳備用資料</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { success: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, error: error.message }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br></div></div><h3 id="並行處理" tabindex="-1">並行處理 <a class="header-anchor" href="#並行處理" aria-label="Permalink to “並行處理”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 序列執行（慢）</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchAllData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> users</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchUsers</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()      </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 等待 1 秒</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> products</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchProducts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 等待 1 秒</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> orders</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchOrders</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()     </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 等待 1 秒</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 總共 3 秒</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 並行執行（快）</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchAllData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">users</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">products</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">orders</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> Promise</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">all</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    fetchUsers</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    fetchProducts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    fetchOrders</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ])</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 總共約 1 秒（取最慢的那個）</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><h2 id="物件與陣列操作" tabindex="-1">物件與陣列操作 <a class="header-anchor" href="#物件與陣列操作" aria-label="Permalink to “物件與陣列操作”">&#8203;</a></h2>
<h3 id="不可變性-immutability" tabindex="-1">不可變性（Immutability） <a class="header-anchor" href="#不可變性-immutability" aria-label="Permalink to “不可變性（Immutability）”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 直接修改原始資料</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> addItem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">cart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">item</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  cart.items.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">push</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(item)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  cart.total </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> item.price</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cart</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 建立新物件</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> addItem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">cart</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">item</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    ...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">cart,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    items: [</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">cart.items, item],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    total: cart.total </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> item.price</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br></div></div><h3 id="善用陣列方法" tabindex="-1">善用陣列方法 <a class="header-anchor" href="#善用陣列方法" aria-label="Permalink to “善用陣列方法”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 使用 for 迴圈</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> activeUsers</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">let</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> users.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; i</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">++</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (users[i].isActive) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    activeUsers.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">push</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(users[i].name)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 使用 filter 和 map</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> activeUsers</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> users</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.isActive)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.name)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><h3 id="optional-chaining-與-nullish-coalescing" tabindex="-1">Optional Chaining 與 Nullish Coalescing <a class="header-anchor" href="#optional-chaining-與-nullish-coalescing" aria-label="Permalink to “Optional Chaining 與 Nullish Coalescing”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 冗長的檢查</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> city</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.address </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.address.city</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> name</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> undefined</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  ?</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.name</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  :</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'Anonymous'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 使用現代語法</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> city</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user?.address?.city</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> name</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">??</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'Anonymous'</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><h2 id="模組化與職責分離" tabindex="-1">模組化與職責分離 <a class="header-anchor" href="#模組化與職責分離" aria-label="Permalink to “模組化與職責分離”">&#8203;</a></h2>
<h3 id="合理的檔案結構" tabindex="-1">合理的檔案結構 <a class="header-anchor" href="#合理的檔案結構" aria-label="Permalink to “合理的檔案結構”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 所有功能都在一個檔案</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// app.js (1000+ lines)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 按功能拆分模組</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// services/</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">//   userService.js</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">//   authService.js</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// utils/</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">//   validation.js</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">//   formatting.js</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// constants/</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">//   config.js</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><h3 id="匯出的最佳實踐" tabindex="-1">匯出的最佳實踐 <a class="header-anchor" href="#匯出的最佳實踐" aria-label="Permalink to “匯出的最佳實踐”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 預設匯出不利於重構</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculateTotal</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 具名匯出便於追蹤</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculateTotal</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculateTax</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br></div></div><h2 id="註解與文件" tabindex="-1">註解與文件 <a class="header-anchor" href="#註解與文件" aria-label="Permalink to “註解與文件”">&#8203;</a></h2>
<h3 id="何時需要註解" tabindex="-1">何時需要註解 <a class="header-anchor" href="#何時需要註解" aria-label="Permalink to “何時需要註解”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 註解說明顯而易見的事</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 增加 1</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">count</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">++</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 註解彌補爛程式碼</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 這個函式很複雜，需要重構</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> process</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // ...非常複雜的邏輯</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 說明為什麼這樣做</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 使用 debounce 避免在快速輸入時頻繁呼叫 API</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> searchWithDebounce</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> debounce</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(search, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">300</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 標記已知問題</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// TODO: 當使用者權限變更時需要重新驗證 token</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// FIXME: 在 Safari 中會有 memory leak，需要調查</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><h3 id="jsdoc-文件化" tabindex="-1">JSDoc 文件化 <a class="header-anchor" href="#jsdoc-文件化" aria-label="Permalink to “JSDoc 文件化”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/**</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * 計算商品折扣後的價格</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@param</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> {number}</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> price</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> - 原始價格</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@param</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> {number}</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> discount</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> - 折扣百分比 (0-100)</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@returns</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> {number}</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> 折扣後的價格</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> * </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@throws</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> {Error}</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> 當價格或折扣為負數時</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> */</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculateDiscountedPrice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">price</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">discount</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (price </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> discount </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    throw</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Price and discount must be positive'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> price </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> discount </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br></div></div><h2 id="避免常見的反模式" tabindex="-1">避免常見的反模式 <a class="header-anchor" href="#避免常見的反模式" aria-label="Permalink to “避免常見的反模式”">&#8203;</a></h2>
<h3 id="_1-過度抽象" tabindex="-1">1. 過度抽象 <a class="header-anchor" href="#_1-過度抽象" aria-label="Permalink to “1. 過度抽象”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 為了抽象而抽象</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">class</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> AbstractFactoryBuilderManager</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  createAbstractFactory</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ConcreteFactoryBuilder</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 保持簡單</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">data, createdAt: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><h3 id="_2-全域變數污染" tabindex="-1">2. 全域變數污染 <a class="header-anchor" href="#_2-全域變數污染" aria-label="Permalink to “2. 全域變數污染”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 全域變數</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> config </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> userData </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 使用模組或閉包</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> app</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> config</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> userData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    getConfig</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> config,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    getUserData</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> userData</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})()</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br></div></div><h3 id="_3-條件判斷過於複雜" tabindex="-1">3. 條件判斷過於複雜 <a class="header-anchor" href="#_3-條件判斷過於複雜" aria-label="Permalink to “3. 條件判斷過於複雜”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 巢狀過深的條件</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (user) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (user.isActive) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (user.hasPermission) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.isBlocked) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        doSomething</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 提前返回</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.isActive) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.hasPermission) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (user.isBlocked) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">doSomething</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 或使用守衛子句</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> canPerformAction</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user?.isActive </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">         user.hasPermission </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x26;&#x26;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">         !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.isBlocked</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">canPerformAction</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(user)) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  doSomething</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br></div></div><h2 id="總結-整潔程式碼的核心原則" tabindex="-1">總結：整潔程式碼的核心原則 <a class="header-anchor" href="#總結-整潔程式碼的核心原則" aria-label="Permalink to “總結：整潔程式碼的核心原則”">&#8203;</a></h2>
<ol>
<li><strong>可讀性優先</strong>：程式碼是寫給人看的，機器只是順便執行</li>
<li><strong>保持簡單</strong>：KISS (Keep It Simple, Stupid) 原則</li>
<li><strong>單一職責</strong>：每個函式、模組都應該有明確的單一職責</li>
<li><strong>測試友善</strong>：好的程式碼應該容易測試</li>
<li><strong>持續重構</strong>：整潔程式碼不是一次到位，而是持續改進的結果</li>
</ol>
<p>整潔程式碼不是什麼銀彈，也不是要追求完美主義，而是在可讀性、維護性和實用性之間找到平衡。記住一個原則：<strong>程式碼是寫給未來的自己和團隊成員看的</strong>，讓他們能快速理解並安心修改，這才是整潔程式碼的真正價值。</p>
<p>實務上，我會建議搭配 ESLint 和 Prettier 這類工具來自動化程式碼風格檢查，在 Code Review 時也要把程式碼品質當成重要的審查項目。這樣才能在團隊中慢慢建立起良好的程式碼文化。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>javascript</category>
            <category>clean-code</category>
            <category>程式設計</category>
            <category>前端開發</category>
        </item>
        <item>
            <title><![CDATA[Vite 專案效能最佳化實戰指南]]></title>
            <link>https://kurohsu.dev/notes/vite-optimization-guide.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/vite-optimization-guide.html</guid>
            <pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="vite-專案效能最佳化實戰指南" tabindex="-1">Vite 專案效能最佳化實戰指南 <a class="header-anchor" href="#vite-專案效能最佳化實戰指南" aria-label="Permalink to “Vite 專案效能最佳化實戰指南”">&#8203;</a></h1>
<p>Vite 的開發體驗真的很爽快，但實際專案跑起來還是有不少可以調校的地方。這篇文章整理了我在開發和生產環境中優化 Vite 專案的一些實用技巧。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="vite-專案效能最佳化實戰指南" tabindex="-1">Vite 專案效能最佳化實戰指南 <a class="header-anchor" href="#vite-專案效能最佳化實戰指南" aria-label="Permalink to “Vite 專案效能最佳化實戰指南”">&#8203;</a></h1>
<p>Vite 的開發體驗真的很爽快，但實際專案跑起來還是有不少可以調校的地方。這篇文章整理了我在開發和生產環境中優化 Vite 專案的一些實用技巧。</p>
<hr>
<h2 id="開發環境最佳化" tabindex="-1">開發環境最佳化 <a class="header-anchor" href="#開發環境最佳化" aria-label="Permalink to “開發環境最佳化”">&#8203;</a></h2>
<h3 id="_1-依賴預建構-dependency-pre-bundling" tabindex="-1">1. 依賴預建構（Dependency Pre-Bundling） <a class="header-anchor" href="#_1-依賴預建構-dependency-pre-bundling" aria-label="Permalink to “1. 依賴預建構（Dependency Pre-Bundling）”">&#8203;</a></h3>
<p>Vite 會自動預建構依賴，但我們可以透過配置加速這個過程：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// vite.config.js</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  optimizeDeps: {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 明確指定需要預建構的依賴</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    include: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      'vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      'vue-router'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      'pinia'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      'axios'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 深層依賴也可以指定</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      'lodash-es'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 排除不需要預建構的依賴</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    exclude: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'@vueuse/core'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br></div></div><p><strong>為什麼要這樣做？</strong></p>
<ul>
<li>減少首次啟動時的依賴掃描時間</li>
<li>避免某些套件在運行時被重複轉換</li>
<li>特別是對於有大量依賴的專案，可以明顯提升啟動速度</li>
</ul>
<h3 id="_2-模組熱更新-hmr-最佳化" tabindex="-1">2. 模組熱更新（HMR）最佳化 <a class="header-anchor" href="#_2-模組熱更新-hmr-最佳化" aria-label="Permalink to “2. 模組熱更新（HMR）最佳化”">&#8203;</a></h3>
<p>合理的元件拆分可以提升 HMR 的效率：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 不好的做法：單一巨大檔案</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// App.vue 包含所有邏輯、樣式、子元件</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 好的做法：拆分元件和樣式</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// App.vue</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Header </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './components/Header.vue'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Content </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './components/Content.vue'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Footer </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './components/Footer.vue'</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p><strong>技巧：</strong></p>
<ul>
<li>將大型元件拆分成更小的單元</li>
<li>樣式檔案獨立管理，避免 CSS 和 JS 綁定</li>
<li>使用 <code>&lt;script setup&gt;</code> 語法，Vite 對其有更好的 HMR 支援</li>
</ul>
<h3 id="_3-路徑別名配置" tabindex="-1">3. 路徑別名配置 <a class="header-anchor" href="#_3-路徑別名配置" aria-label="Permalink to “3. 路徑別名配置”">&#8203;</a></h3>
<p>設定路徑別名不僅提升開發體驗，也能加速模組解析：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// vite.config.js</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { fileURLToPath, URL } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'node:url'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  resolve: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    alias: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      '@'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">fileURLToPath</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> URL</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'./src'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">meta</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.url)),</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      '@components'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">fileURLToPath</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> URL</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'./src/components'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">meta</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.url)),</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      '@utils'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">fileURLToPath</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> URL</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'./src/utils'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">meta</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.url))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><h2 id="生產環境最佳化" tabindex="-1">生產環境最佳化 <a class="header-anchor" href="#生產環境最佳化" aria-label="Permalink to “生產環境最佳化”">&#8203;</a></h2>
<h3 id="_1-code-splitting-策略" tabindex="-1">1. Code Splitting 策略 <a class="header-anchor" href="#_1-code-splitting-策略" aria-label="Permalink to “1. Code Splitting 策略”">&#8203;</a></h3>
<h4 id="路由層級的分割" tabindex="-1">路由層級的分割 <a class="header-anchor" href="#路由層級的分割" aria-label="Permalink to “路由層級的分割”">&#8203;</a></h4>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// router/index.js</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> routes</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    path: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/dashboard'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 使用動態 import</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'@/views/Dashboard.vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    path: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/settings'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'@/views/Settings.vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><h4 id="元件層級的分割" tabindex="-1">元件層級的分割 <a class="header-anchor" href="#元件層級的分割" aria-label="Permalink to “元件層級的分割”">&#8203;</a></h4>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { defineAsyncComponent } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 大型元件使用 lazy loading</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> HeavyChart</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineAsyncComponent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'@/components/HeavyChart.vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><h4 id="手動分割-chunk" tabindex="-1">手動分割 chunk <a class="header-anchor" href="#手動分割-chunk" aria-label="Permalink to “手動分割 chunk”">&#8203;</a></h4>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// vite.config.js</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  build: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    rollupOptions: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      output: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        manualChunks: {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">          // 將 Vue 生態系分離出來</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">          'vue-vendor'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'vue-router'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'pinia'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">          // UI 框架獨立打包</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">          'ui-vendor'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'element-plus'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">          // 工具函式庫</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">          'utils'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'lodash-es'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'dayjs'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><p><strong>為什麼要分割 chunk？</strong></p>
<ul>
<li>提升瀏覽器快取效率</li>
<li>第三方庫變動頻率低，分離後可以長期快取</li>
<li>減少首次載入的 bundle 大小</li>
</ul>
<h3 id="_2-壓縮與最佳化" tabindex="-1">2. 壓縮與最佳化 <a class="header-anchor" href="#_2-壓縮與最佳化" aria-label="Permalink to “2. 壓縮與最佳化”">&#8203;</a></h3>
<h4 id="gzip-brotli-壓縮" tabindex="-1">Gzip/Brotli 壓縮 <a class="header-anchor" href="#gzip-brotli-壓縮" aria-label="Permalink to “Gzip/Brotli 壓縮”">&#8203;</a></h4>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// vite.config.js</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> viteCompression </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vite-plugin-compression'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  plugins: [</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    viteCompression</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      algorithm: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'gzip'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ext: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.gz'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }),</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    viteCompression</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      algorithm: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'brotliCompress'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ext: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.br'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br></div></div><h4 id="css-最佳化" tabindex="-1">CSS 最佳化 <a class="header-anchor" href="#css-最佳化" aria-label="Permalink to “CSS 最佳化”">&#8203;</a></h4>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  css: {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 生產環境移除未使用的 CSS</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    postcss: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      plugins: [</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        require</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'autoprefixer'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        require</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'cssnano'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          preset: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'default'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br></div></div><h3 id="_3-圖片最佳化" tabindex="-1">3. 圖片最佳化 <a class="header-anchor" href="#_3-圖片最佳化" aria-label="Permalink to “3. 圖片最佳化”">&#8203;</a></h3>
<p>使用 Vite 插件處理圖片：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { ViteImageOptimizer } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vite-plugin-image-optimizer'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  plugins: [</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    ViteImageOptimizer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      png: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        quality: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">80</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      jpeg: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        quality: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">80</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      webp: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        quality: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">80</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><p><strong>實務建議：</strong></p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  &#x3C;!-- 使用現代圖片格式 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">picture</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">source</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> srcset</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/images/hero.webp"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"image/webp"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">source</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> srcset</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/images/hero.jpg"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"image/jpeg"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">img</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/images/hero.jpg"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> alt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Hero image"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">picture</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><h3 id="_4-tree-shaking-最佳化" tabindex="-1">4. Tree Shaking 最佳化 <a class="header-anchor" href="#_4-tree-shaking-最佳化" aria-label="Permalink to “4. Tree Shaking 最佳化”">&#8203;</a></h3>
<p>確保套件支援 tree shaking：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 不利於 tree shaking</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> _ </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'lodash'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 使用 ES modules 版本</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { debounce, throttle } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'lodash-es'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 直接引用需要的函式</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> debounce </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'lodash-es/debounce'</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><h2 id="build-效能最佳化" tabindex="-1">Build 效能最佳化 <a class="header-anchor" href="#build-效能最佳化" aria-label="Permalink to “Build 效能最佳化”">&#8203;</a></h2>
<h3 id="_1-使用-esbuild-或-swc" tabindex="-1">1. 使用 esbuild 或 SWC <a class="header-anchor" href="#_1-使用-esbuild-或-swc" aria-label="Permalink to “1. 使用 esbuild 或 SWC”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  build: {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 使用 esbuild 壓縮（預設）</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    minify: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'esbuild'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 或使用 terser 獲得更好的壓縮率</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // minify: 'terser',</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><h3 id="_2-平行處理" tabindex="-1">2. 平行處理 <a class="header-anchor" href="#_2-平行處理" aria-label="Permalink to “2. 平行處理”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  build: {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 啟用多執行緒建構</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    terserOptions: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      compress: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        parallel: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><h3 id="_3-source-map-策略" tabindex="-1">3. Source Map 策略 <a class="header-anchor" href="#_3-source-map-策略" aria-label="Permalink to “3. Source Map 策略”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  build: {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 開發環境使用完整 source map</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    sourcemap: process.env.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">NODE_ENV</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'development'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> :</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 生產環境可以選擇隱藏或使用輕量版</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // sourcemap: 'hidden'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><h2 id="效能監控" tabindex="-1">效能監控 <a class="header-anchor" href="#效能監控" aria-label="Permalink to “效能監控”">&#8203;</a></h2>
<h3 id="使用-vite-bundle-analyzer" tabindex="-1">使用 Vite Bundle Analyzer <a class="header-anchor" href="#使用-vite-bundle-analyzer" aria-label="Permalink to “使用 Vite Bundle Analyzer”">&#8203;</a></h3>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { visualizer } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'rollup-plugin-visualizer'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  plugins: [</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    visualizer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      open: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      gzipSize: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      brotliSize: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><p>這會生成一個互動式的 bundle 分析圖，幫助你找出可以最佳化的部分。</p>
<h2 id="實際案例-專案最佳化前後對比" tabindex="-1">實際案例：專案最佳化前後對比 <a class="header-anchor" href="#實際案例-專案最佳化前後對比" aria-label="Permalink to “實際案例：專案最佳化前後對比”">&#8203;</a></h2>
<p>在一個中型專案中，應用上述技巧後的效果：</p>
<p><strong>開發環境：</strong></p>
<ul>
<li>啟動時間：從 3.2s 降至 1.8s（44% 提升）</li>
<li>HMR 更新：從 800ms 降至 200ms</li>
</ul>
<p><strong>生產環境：</strong></p>
<ul>
<li>首次載入時間：從 2.1s 降至 1.2s</li>
<li>Bundle 大小：從 850KB 降至 420KB（使用 gzip）</li>
</ul>
<h2 id="總結" tabindex="-1">總結 <a class="header-anchor" href="#總結" aria-label="Permalink to “總結”">&#8203;</a></h2>
<p>優化 Vite 專案主要抓住幾個重點：</p>
<ol>
<li><strong>開發環境</strong>：專注於啟動速度和 HMR 效率</li>
<li><strong>生產環境</strong>：著重於 bundle 大小和載入效能</li>
<li><strong>持續監控</strong>：使用分析工具找出瓶頸</li>
</ol>
<p>效能優化是個持續的過程，要看專案實際狀況來調整。記住一個原則：<strong>不要過早優化</strong>，先用工具找出真正的瓶頸在哪裡，再針對性地解決。</p>
<p>我的建議是養成定期檢查 bundle 分析的習慣，這樣才能確保效能不會隨著專案成長而越來越糟。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vite</category>
            <category>效能優化</category>
            <category>前端開發</category>
            <category>build-tools</category>
        </item>
        <item>
            <title><![CDATA[Vue 3 Composition API 實戰模式與最佳實踐]]></title>
            <link>https://kurohsu.dev/notes/vue3-composition-api-patterns.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/vue3-composition-api-patterns.html</guid>
            <pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="vue-3-composition-api-實戰模式與最佳實踐" tabindex="-1">Vue 3 Composition API 實戰模式與最佳實踐 <a class="header-anchor" href="#vue-3-composition-api-實戰模式與最佳實踐" aria-label="Permalink to “Vue 3 Composition API 實戰模式與最佳實踐”">&#8203;</a></h1>
<p>Vue 3 的 Composition API 提供了更靈活的程式碼組織方式，但實際用起來卻讓不少人感到困惑。這篇文章想跟你分享我這幾年使用 Composition API 的實戰經驗和常用模式。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="vue-3-composition-api-實戰模式與最佳實踐" tabindex="-1">Vue 3 Composition API 實戰模式與最佳實踐 <a class="header-anchor" href="#vue-3-composition-api-實戰模式與最佳實踐" aria-label="Permalink to “Vue 3 Composition API 實戰模式與最佳實踐”">&#8203;</a></h1>
<p>Vue 3 的 Composition API 提供了更靈活的程式碼組織方式，但實際用起來卻讓不少人感到困惑。這篇文章想跟你分享我這幾年使用 Composition API 的實戰經驗和常用模式。</p>
<hr>
<h2 id="為什麼選擇-composition-api" tabindex="-1">為什麼選擇 Composition API？ <a class="header-anchor" href="#為什麼選擇-composition-api" aria-label="Permalink to “為什麼選擇 Composition API？”">&#8203;</a></h2>
<p>在開始之前，先來聊聊 Composition API 到底解決了什麼問題：</p>
<ol>
<li><strong>邏輯複用性</strong>：Options API 在複用邏輯時常需要使用 mixins，容易產生命名衝突和來源不明的問題</li>
<li><strong>程式碼組織</strong>：Options API 將相關邏輯分散在不同的選項中（data、methods、computed），不利於閱讀和維護</li>
<li><strong>型別推導</strong>：Composition API 對 TypeScript 的支援更加友善</li>
</ol>
<h2 id="實戰模式一-composable-函式的設計原則" tabindex="-1">實戰模式一：Composable 函式的設計原則 <a class="header-anchor" href="#實戰模式一-composable-函式的設計原則" aria-label="Permalink to “實戰模式一：Composable 函式的設計原則”">&#8203;</a></h2>
<h3 id="單一職責原則" tabindex="-1">單一職責原則 <a class="header-anchor" href="#單一職責原則" aria-label="Permalink to “單一職責原則”">&#8203;</a></h3>
<p>一個好的 composable 應該只專注於一件事。以使用者認證為例：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 不好的做法：職責過多</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useAuth</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> isLoggedIn</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> notifications</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([])</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> settings</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 混雜了認證、通知、設定等多個功能</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 好的做法：拆分成多個 composable</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useAuth</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> isLoggedIn</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> computed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">user.value)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> login</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">credentials</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 登入邏輯</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> logout</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    user.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { user, isLoggedIn, login, logout }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useNotifications</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 專注於通知功能</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useUserSettings</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 專注於使用者設定</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br></div></div><h3 id="命名規範" tabindex="-1">命名規範 <a class="header-anchor" href="#命名規範" aria-label="Permalink to “命名規範”">&#8203;</a></h3>
<p>Composable 函式應該：</p>
<ul>
<li>以 <code>use</code> 開頭（遵循 Vue 社群慣例）</li>
<li>使用動詞或名詞描述功能</li>
<li>清楚表達其職責</li>
</ul>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 推薦的命名</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">useMousePosition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">useFetch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">useLocalStorage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">useFormValidation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><h2 id="實戰模式二-狀態管理的分層架構" tabindex="-1">實戰模式二：狀態管理的分層架構 <a class="header-anchor" href="#實戰模式二-狀態管理的分層架構" aria-label="Permalink to “實戰模式二：狀態管理的分層架構”">&#8203;</a></h2>
<p>在大型專案中，我建議採用分層的狀態管理策略：</p>
<h3 id="_1-元件級狀態-component-state" tabindex="-1">1. 元件級狀態（Component State） <a class="header-anchor" href="#_1-元件級狀態-component-state" aria-label="Permalink to “1. 元件級狀態（Component State）”">&#8203;</a></h3>
<p>僅在單一元件內使用的狀態，直接使用 <code>ref</code> 或 <code>reactive</code>：</p>
<div class="language-vue line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { ref } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> isOpen</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> toggleMenu</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  isOpen.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">isOpen.value</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><h3 id="_2-共享狀態-shared-state" tabindex="-1">2. 共享狀態（Shared State） <a class="header-anchor" href="#_2-共享狀態-shared-state" aria-label="Permalink to “2. 共享狀態（Shared State）”">&#8203;</a></h3>
<p>跨多個元件共享的狀態，使用 composable 封裝：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// composables/useCounter.js</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { ref } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 在模組作用域中定義，所有使用此 composable 的元件共享同一個狀態</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> count</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useCounter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> increment</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    count.value</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">++</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> decrement</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    count.value</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">--</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { count: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">readonly</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(count), increment, decrement }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><h3 id="_3-全域狀態-global-state" tabindex="-1">3. 全域狀態（Global State） <a class="header-anchor" href="#_3-全域狀態-global-state" aria-label="Permalink to “3. 全域狀態（Global State）”">&#8203;</a></h3>
<p>複雜的應用級狀態，考慮使用 Pinia：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// stores/user.js</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { defineStore } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'pinia'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> useUserStore</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> defineStore</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'user'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> permissions</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> computed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> user.value?.permissions </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [])</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetchUser</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // API 呼叫</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { user, permissions, fetchUser }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br></div></div><h2 id="實戰模式三-副作用的處理" tabindex="-1">實戰模式三：副作用的處理 <a class="header-anchor" href="#實戰模式三-副作用的處理" aria-label="Permalink to “實戰模式三：副作用的處理”">&#8203;</a></h2>
<h3 id="資料獲取-data-fetching" tabindex="-1">資料獲取（Data Fetching） <a class="header-anchor" href="#資料獲取-data-fetching" aria-label="Permalink to “資料獲取（Data Fetching）”">&#8203;</a></h3>
<p>處理非同步資料獲取時，建議封裝載入狀態和錯誤處理：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useFetch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">url</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> data</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> error</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> loading</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> fetch</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    loading.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    error.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> response</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> axios.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(url.value)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      data.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> response.data</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (e) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      error.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> e</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">finally</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      loading.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 自動執行</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  watchEffect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    fetch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { data, error, loading, refetch: fetch }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br></div></div><h3 id="事件監聽的清理" tabindex="-1">事件監聽的清理 <a class="header-anchor" href="#事件監聽的清理" aria-label="Permalink to “事件監聽的清理”">&#8203;</a></h3>
<p>使用 <code>onUnmounted</code> 確保事件監聽器被正確清理：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> useEventListener</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">target</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">event</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">handler</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  onMounted</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    target.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">addEventListener</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(event, handler)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  onUnmounted</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    target.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">removeEventListener</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(event, handler)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><h2 id="實戰模式四-效能最佳化" tabindex="-1">實戰模式四：效能最佳化 <a class="header-anchor" href="#實戰模式四-效能最佳化" aria-label="Permalink to “實戰模式四：效能最佳化”">&#8203;</a></h2>
<h3 id="避免不必要的響應式" tabindex="-1">避免不必要的響應式 <a class="header-anchor" href="#避免不必要的響應式" aria-label="Permalink to “避免不必要的響應式”">&#8203;</a></h3>
<p>不是所有資料都需要是響應式的：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ❌ 不必要的響應式</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> config</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> reactive</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  apiUrl: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://api.example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  timeout: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">5000</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 常數不需要響應式</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> CONFIG</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  apiUrl: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://api.example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  timeout: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">5000</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><h3 id="善用-computed-快取" tabindex="-1">善用 computed 快取 <a class="header-anchor" href="#善用-computed-快取" aria-label="Permalink to “善用 computed 快取”">&#8203;</a></h3>
<p><code>computed</code> 會快取計算結果，只在相依的響應式資料變化時重新計算：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ✅ 使用 computed 快取昂貴的計算</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> expensiveValue</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> computed</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> items.value.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">reduce</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">sum</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">item</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 複雜計算</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> sum </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> calculateScore</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(item)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><h3 id="大型列表的效能優化" tabindex="-1">大型列表的效能優化 <a class="header-anchor" href="#大型列表的效能優化" aria-label="Permalink to “大型列表的效能優化”">&#8203;</a></h3>
<p>對於大型列表，考慮使用 <code>shallowRef</code> 或 <code>shallowReactive</code>：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 當不需要深層響應式時</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> largeList</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> shallowRef</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([])</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 更新整個陣列</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">largeList.value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> newList</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><h2 id="總結" tabindex="-1">總結 <a class="header-anchor" href="#總結" aria-label="Permalink to “總結”">&#8203;</a></h2>
<p>Composition API 雖然強大，但也需要遵循一些基本原則：</p>
<ol>
<li><strong>保持 composable 的單一職責</strong></li>
<li><strong>根據狀態的作用域選擇合適的管理方式</strong></li>
<li><strong>妥善處理副作用和生命週期</strong></li>
<li><strong>關注效能，避免過度使用響應式</strong></li>
</ol>
<p>這些模式不是什麼絕對真理，而是我在實務開發中踩過坑後的心得。實際用的時候還是要看專案狀況彈性調整。最重要的是讓程式碼保持可讀性和可維護性，這樣團隊成員才能順暢協作。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue</category>
            <category>composition-api</category>
            <category>javascript</category>
            <category>前端開發</category>
        </item>
        <item>
            <title><![CDATA[咖啡廳隨筆]]></title>
            <link>https://kurohsu.dev/misc/coffee-shop-notes.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/misc/coffee-shop-notes.html</guid>
            <pubDate>Tue, 28 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="咖啡廳隨筆" tabindex="-1">咖啡廳隨筆 <a class="header-anchor" href="#咖啡廳隨筆" aria-label="Permalink to “咖啡廳隨筆”">&#8203;</a></h1>
<p>週末下午在附近新開的咖啡廳工作，意外地發現了一個適合專注的好地方。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="咖啡廳隨筆" tabindex="-1">咖啡廳隨筆 <a class="header-anchor" href="#咖啡廳隨筆" aria-label="Permalink to “咖啡廳隨筆”">&#8203;</a></h1>
<p>週末下午在附近新開的咖啡廳工作，意外地發現了一個適合專注的好地方。</p>
<hr>
<h2 id="環境" tabindex="-1">環境 <a class="header-anchor" href="#環境" aria-label="Permalink to “環境”">&#8203;</a></h2>
<p>這家店位於巷弄中，沒有太多喧囂。木質裝潢配上柔和的燈光，營造出溫暖舒適的氛圍。</p>
<h2 id="咖啡" tabindex="-1">咖啡 <a class="header-anchor" href="#咖啡" aria-label="Permalink to “咖啡”">&#8203;</a></h2>
<p>點了一杯手沖耶加雪菲：</p>
<ul>
<li>入口有明顯的花香和柑橘調性</li>
<li>酸度適中，不會太刺激</li>
<li>尾韻帶點淡淡的茶感</li>
</ul>
<h2 id="工作效率" tabindex="-1">工作效率 <a class="header-anchor" href="#工作效率" aria-label="Permalink to “工作效率”">&#8203;</a></h2>
<p>在這裡待了三個小時，完成了：</p>
<ul>
<li>兩個 bug 修復</li>
<li>一份技術文件</li>
<li>規劃下週的工作項目</li>
</ul>
<p>有時候換個環境工作，真的會有不一樣的效率和心情。</p>
<h2 id="小結" tabindex="-1">小結 <a class="header-anchor" href="#小結" aria-label="Permalink to “小結”">&#8203;</a></h2>
<p>會想再來的咖啡廳 ☕️</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>生活</category>
            <category>咖啡</category>
            <category>隨筆</category>
        </item>
        <item>
            <title><![CDATA[關於生產力的一些思考]]></title>
            <link>https://kurohsu.dev/misc/productivity-thoughts.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/misc/productivity-thoughts.html</guid>
            <pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="關於生產力的一些思考" tabindex="-1">關於生產力的一些思考 <a class="header-anchor" href="#關於生產力的一些思考" aria-label="Permalink to “關於生產力的一些思考”">&#8203;</a></h1>
<p>最近在重新審視自己的工作方式，發現一些有趣的觀察。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="關於生產力的一些思考" tabindex="-1">關於生產力的一些思考 <a class="header-anchor" href="#關於生產力的一些思考" aria-label="Permalink to “關於生產力的一些思考”">&#8203;</a></h1>
<p>最近在重新審視自己的工作方式，發現一些有趣的觀察。</p>
<hr>
<h2 id="番茄工作法的變體" tabindex="-1">番茄工作法的變體 <a class="header-anchor" href="#番茄工作法的變體" aria-label="Permalink to “番茄工作法的變體”">&#8203;</a></h2>
<p>傳統的 25 分鐘對我來說太短了，調整成：</p>
<ul>
<li><strong>工作時段</strong>: 50 分鐘</li>
<li><strong>短休息</strong>: 10 分鐘</li>
<li><strong>長休息</strong>: 30 分鐘（每 2 個循環後）</li>
</ul>
<p>這個節奏更符合我進入深度工作的時間需求。</p>
<h2 id="任務清單的藝術" tabindex="-1">任務清單的藝術 <a class="header-anchor" href="#任務清單的藝術" aria-label="Permalink to “任務清單的藝術”">&#8203;</a></h2>
<h3 id="以前的做法" tabindex="-1">以前的做法 <a class="header-anchor" href="#以前的做法" aria-label="Permalink to “以前的做法”">&#8203;</a></h3>
<p>把所有待辦事項都列出來，結果：</p>
<ul>
<li>清單越來越長</li>
<li>看到就有壓力</li>
<li>很多項目一直沒完成</li>
</ul>
<h3 id="現在的做法" tabindex="-1">現在的做法 <a class="header-anchor" href="#現在的做法" aria-label="Permalink to “現在的做法”">&#8203;</a></h3>
<p>只列「今天必須完成」的 3-5 項：</p>
<ul>
<li>更聚焦</li>
<li>完成後有成就感</li>
<li>其他事項放在「未來」清單</li>
</ul>
<h2 id="深度工作時段" tabindex="-1">深度工作時段 <a class="header-anchor" href="#深度工作時段" aria-label="Permalink to “深度工作時段”">&#8203;</a></h2>
<p>發現自己在這些時段最有生產力：</p>
<ol>
<li><strong>早上 9:00-12:00</strong>: 適合處理複雜問題</li>
<li><strong>下午 2:00-4:00</strong>: 適合寫文件和規劃</li>
<li><strong>晚上 8:00-10:00</strong>: 適合學習新東西</li>
</ol>
<h2 id="工具不是重點" tabindex="-1">工具不是重點 <a class="header-anchor" href="#工具不是重點" aria-label="Permalink to “工具不是重點”">&#8203;</a></h2>
<p>試過各種生產力工具，最後發現：</p>
<ul>
<li>工具只是輔助</li>
<li>重要的是找到適合自己的節奏</li>
<li>簡單的方法往往最有效</li>
</ul>
<p>持續實驗中 🚀</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>效率</category>
            <category>思考</category>
            <category>工作方法</category>
        </item>
        <item>
            <title><![CDATA[週末料理實驗]]></title>
            <link>https://kurohsu.dev/misc/weekend-cooking.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/misc/weekend-cooking.html</guid>
            <pubDate>Fri, 24 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="週末料理實驗" tabindex="-1">週末料理實驗 <a class="header-anchor" href="#週末料理實驗" aria-label="Permalink to “週末料理實驗”">&#8203;</a></h1>
<p>決定在週末挑戰做一些平常不會嘗試的料理。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="週末料理實驗" tabindex="-1">週末料理實驗 <a class="header-anchor" href="#週末料理實驗" aria-label="Permalink to “週末料理實驗”">&#8203;</a></h1>
<p>決定在週末挑戰做一些平常不會嘗試的料理。</p>
<hr>
<h2 id="菜單" tabindex="-1">菜單 <a class="header-anchor" href="#菜單" aria-label="Permalink to “菜單”">&#8203;</a></h2>
<h3 id="主菜-香煎鮭魚" tabindex="-1">主菜：香煎鮭魚 <a class="header-anchor" href="#主菜-香煎鮭魚" aria-label="Permalink to “主菜：香煎鮭魚”">&#8203;</a></h3>
<ul>
<li>鮭魚切片，兩面撒鹽和黑胡椒</li>
<li>熱鍋後中火煎，每面 3-4 分鐘</li>
<li>起鍋前加入奶油和蒜片</li>
</ul>
<p><strong>心得</strong>：火候控制很重要，中火剛好可以讓表面金黃，內部保持濕潤。</p>
<h3 id="配菜-烤時蔬" tabindex="-1">配菜：烤時蔬 <a class="header-anchor" href="#配菜-烤時蔬" aria-label="Permalink to “配菜：烤時蔬”">&#8203;</a></h3>
<p>準備的蔬菜：</p>
<ul>
<li>櫛瓜</li>
<li>彩椒</li>
<li>杏鮑菇</li>
<li>小番茄</li>
</ul>
<p>切好後淋橄欖油，加鹽、黑胡椒和義式香料，200°C 烤 20 分鐘。</p>
<h3 id="甜點-舒芙蕾鬆餅" tabindex="-1">甜點：舒芙蕾鬆餅 <a class="header-anchor" href="#甜點-舒芙蕾鬆餅" aria-label="Permalink to “甜點：舒芙蕾鬆餅”">&#8203;</a></h3>
<p>這個失敗了 😅</p>
<p><strong>問題分析</strong>：</p>
<ul>
<li>蛋白打發不夠</li>
<li>火太大導致外面焦了裡面還沒熟</li>
<li>翻面時機沒抓好</li>
</ul>
<p>下次要改進的地方：</p>
<ol>
<li>蛋白要打到完全挺立</li>
<li>用小火慢慢煎</li>
<li>看到邊緣開始凝固再翻面</li>
</ol>
<h2 id="學到的事" tabindex="-1">學到的事 <a class="header-anchor" href="#學到的事" aria-label="Permalink to “學到的事”">&#8203;</a></h2>
<ol>
<li><strong>準備工作很重要</strong>：所有食材都先切好、調料都先量好，做起來就很順</li>
<li><strong>不要同時做太多事</strong>：專心做好一道再做下一道</li>
<li><strong>失敗也是學習</strong>：舒芙蕾鬆餅雖然失敗了，但知道問題在哪了</li>
</ol>
<h2 id="下次想嘗試" tabindex="-1">下次想嘗試 <a class="header-anchor" href="#下次想嘗試" aria-label="Permalink to “下次想嘗試”">&#8203;</a></h2>
<ul>
<li>手工義大利麵</li>
<li>紅酒燉牛肉</li>
<li>提拉米蘇</li>
</ul>
<p>料理真的是很療癒的活動 🍳</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>料理</category>
            <category>生活</category>
            <category>烹飪</category>
        </item>
        <item>
            <title><![CDATA[Clean Code 短記]]></title>
            <link>https://kurohsu.dev/learn/reading-notes-clean-code.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/learn/reading-notes-clean-code.html</guid>
            <pubDate>Sat, 30 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="clean-code-短記" tabindex="-1">Clean Code 短記 <a class="header-anchor" href="#clean-code-短記" aria-label="Permalink to “Clean Code 短記”">&#8203;</a></h1>
<p>Robert C. Martin 的《Clean Code》是每個開發者都應該閱讀的經典著作。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="clean-code-短記" tabindex="-1">Clean Code 短記 <a class="header-anchor" href="#clean-code-短記" aria-label="Permalink to “Clean Code 短記”">&#8203;</a></h1>
<p>Robert C. Martin 的《Clean Code》是每個開發者都應該閱讀的經典著作。</p>
<hr>
<h2 id="重點摘要" tabindex="-1">重點摘要 <a class="header-anchor" href="#重點摘要" aria-label="Permalink to “重點摘要”">&#8203;</a></h2>
<h3 id="有意義的命名" tabindex="-1">有意義的命名 <a class="header-anchor" href="#有意義的命名" aria-label="Permalink to “有意義的命名”">&#8203;</a></h3>
<ul>
<li>使用能夠表達意圖的名稱</li>
<li>避免誤導性的命名</li>
<li>使用可搜尋的名稱</li>
</ul>
<h3 id="函數應該短小" tabindex="-1">函數應該短小 <a class="header-anchor" href="#函數應該短小" aria-label="Permalink to “函數應該短小”">&#8203;</a></h3>
<ul>
<li>一個函數只做一件事</li>
<li>函數應該只有一層抽象層級</li>
</ul>
<h2 id="我的心得" tabindex="-1">我的心得 <a class="header-anchor" href="#我的心得" aria-label="Permalink to “我的心得”">&#8203;</a></h2>
<p>閱讀這本書讓我重新思考程式碼品質的重要性。好的程式碼不只是能運行，更要容易理解和維護。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>note</category>
            <category>clean-code</category>
        </item>
        <item>
            <title><![CDATA[VueJS 元件載入模板 (template) 的幾種方式]]></title>
            <link>https://kurohsu.dev/notes/VueJS-元件載入模板-template-的幾種方式.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/VueJS-元件載入模板-template-的幾種方式.html</guid>
            <pubDate>Thu, 21 Sep 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="vuejs-元件載入模板-template-的幾種方式" tabindex="-1">VueJS 元件載入模板 (template) 的幾種方式 <a class="header-anchor" href="#vuejs-元件載入模板-template-的幾種方式" aria-label="Permalink to “VueJS 元件載入模板 (template) 的幾種方式”">&#8203;</a></h1>
<img src="/images/notes/vue-template.png">
<p>平常已經在使用 VueJS 開發專案的朋友，相信對 Vue Components 的用法已經不陌生，
而 Component 有個相當棒的特性，就是將 HTML 封裝起來，掛載在網頁上的時候，只需要透過自定義的 tag 如 <code>&lt;components&gt;&lt;/components&gt;</code> 就可以掛載至網頁上。</p>
<hr>
<p>如果你是透過 CDN 載入 VueJS 做開發的朋友，相信最熟悉的方式應該是 in-HTML Templates 的方式：</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="vuejs-元件載入模板-template-的幾種方式" tabindex="-1">VueJS 元件載入模板 (template) 的幾種方式 <a class="header-anchor" href="#vuejs-元件載入模板-template-的幾種方式" aria-label="Permalink to “VueJS 元件載入模板 (template) 的幾種方式”">&#8203;</a></h1>
<img src="/images/notes/vue-template.png">
<p>平常已經在使用 VueJS 開發專案的朋友，相信對 Vue Components 的用法已經不陌生，
而 Component 有個相當棒的特性，就是將 HTML 封裝起來，掛載在網頁上的時候，只需要透過自定義的 tag 如 <code>&lt;components&gt;&lt;/components&gt;</code> 就可以掛載至網頁上。</p>
<hr>
<p>如果你是透過 CDN 載入 VueJS 做開發的朋友，相信最熟悉的方式應該是 in-HTML Templates 的方式：</p>
<hr>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ hello }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"text/javascript"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> vm </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  data: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    hello: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Hello, World!'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><hr>
<p>當我們開始切分子元件 (Child Components) 之後，通常會使用到 <code>template</code> 這個 option，然後裡面可以填入 HTML 的模板字串：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">greeter</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"World"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">greeter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Vue.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'greeter'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  template: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x3C;div> Hello, {{ name }}!&#x3C;/div>'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  props: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'name'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><hr>
<p>然而，隨著專案規模的擴增，我們的 HTML 模板結構可能會變得越來越大，光是用 <code>template</code> 直接掛上 HTML 字串時，可能你的程式架構就會變得不是那麼好閱讀、管理，這時候，我們可以把整個 HTML 模板區塊透過 <code>&lt;script id=&quot;xxx&quot; type=&quot;text/x-template&quot;&gt; &lt;/script&gt;</code> 這樣的 <code>&lt;script&gt;</code> 標籤來封裝我們的 HTML 模板，這種方式通常被稱為「X-Templates」：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"text/x-template"</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"my-component"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"component"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>A custom component of Vue!&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>然後在 template 的 option 可以直接帶入對應的 CSS Selector，像這樣：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Vue.component('my-component', {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  template: '#my-component'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><hr>
<p>再來，如果你有聽過 Vue Components 的編譯作用域 (Compilation Scope) 的話，你應該會知道在子元件中帶入的任何 tag 是沒有意義的：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">child-component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  {{ message }}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">child-component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>像上面這樣，<code>&lt;child-component&gt;</code> 在經過編譯之後，會直接把 {{ &quot;{{ message &quot; }}}} 忽略掉。</p>
<p>如果這種時候，你又非得要在 <code>&lt;child-component&gt;</code> 安插 HTML 模板不可時，你就可以透過 <code>inline-template</code> 這個屬性來幫忙：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">my-component</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> inline-template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>These are compiled as the component's own template.&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>Not parent's transclusion content.&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">my-component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>但要小心，加入了 <code>inline-template</code> 之後，不要跟 <code>&lt;slot&gt;</code> 的有效範圍搞混了，<code>inline-template</code> 的內容是由子元件提供，而 <code>&lt;slot&gt;</code> 的內容是由父層所提供。</p>
<hr>
<p>有寫過 AngularJS 1 的朋友可能會問，過去寫 AngularJS 的時候，directive 裡面有個選項叫做 <code>templateUrl</code>，可以讓我們將模板儲存至另外一個 HTML 檔案，再透過這個 HTML 載入，那麼在 Vue 裡面是否也有類似用法呢？</p>
<p>在講這個之前，我們先來介紹 Vue-loader。
Vue-loader 最大的功能就是他可以將 Vue 的元件封裝成單獨的 .vue 檔案，這個檔案同時包含了三個部分：<code>&lt;template&gt;</code> 、 <code>&lt;script&gt;</code> 以及 <code>&lt;style&gt;</code>。
如果你有用過 Vue CLI 建立專案 (webpack、webpack-simple) 的話，應該對封裝 .vue 檔案的步驟不陌生，這個功能就是 Vue-loader 在背後替我們處理的。</p>
<p>相信大家都知道，既然已經封裝成 .vue 檔案，那麼 HTML 字串模板的部分就可以通通往 <code>&lt;template&gt;</code> 裡面放就好。</p>
<p>回到主題。那麼，如果要透過外部 HTML 檔案載入的話，跟這個有什麼關係？
可能很多人不知道，.vue 檔案的各區塊，其實是可以用 <code>src</code> 屬性來載入外部檔案的。
像這樣，你就可以透過外部的靜態檔案來做為你元件的內容來源：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"./template.html"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"./script.js"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">style</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"./style.css"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>這部分可以参照 Vue-loader 文件的說明： <a href="https://vue-loader.vuejs.org/en/start/spec.html#src-imports" target="_blank" rel="noreferrer">https://vue-loader.vuejs.org/en/start/spec.html#src-imports</a></p>
<p>另外，如果你用的是 laravel-elixir 以及 laravel mix.browserify 已經幫你包裝好的方法的話，更可以直接這樣寫：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">module</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.export </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  template: </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">require</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'./templates/template.html'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      text: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Hello World!"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>template.html 像這樣：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ text }}&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><hr>
<p>上面就是在 VueJS 開發專案上面常見的模版掛載方式，提供給大家參考。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
        </item>
        <item>
            <title><![CDATA[JSDC 全台最大 JS 研討會前直播上線啦！]]></title>
            <link>https://kurohsu.dev/notes/JSDC-全台最大-JS-研討會前直播上線啦！.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/JSDC-全台最大-JS-研討會前直播上線啦！.html</guid>
            <pubDate>Wed, 20 Sep 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="jsdc-全台最大-js-研討會前直播上線啦" tabindex="-1">JSDC 全台最大 JS 研討會前直播上線啦！ <a class="header-anchor" href="#jsdc-全台最大-js-研討會前直播上線啦" aria-label="Permalink to “JSDC 全台最大 JS 研討會前直播上線啦！”">&#8203;</a></h1>
<p>先感謝 JSDC 團隊邀請，這次的直播企劃真的是超快閃，週一晚上接到邀請，週二花了一個小時喬 rundown，然後週三晚上就直接上了，幾乎是沒什麼準備的機會，超刺激。還好直播中沒有什麼太大的意外，也謝謝來自各方的觀眾願意來聽我這個大叔練肖威。</p>
<p>這次直播主要就是做個簡單的訪談，跟大家聊聊出社會這些年來是怎麼踏入前端領域的過程。
<del>順便宣傳一下我今年在 JSDC 分享的議程主題這樣。</del></p>
<p>JSDC 報名傳送門： <a href="https://jsdc-tw.kktix.cc/events/jsdc2017" target="_blank" rel="noreferrer">https://jsdc-tw.kktix.cc/events/jsdc2017</a></p>
<p>可能中間有些胡言亂語的部分，就請大家多包涵。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="jsdc-全台最大-js-研討會前直播上線啦" tabindex="-1">JSDC 全台最大 JS 研討會前直播上線啦！ <a class="header-anchor" href="#jsdc-全台最大-js-研討會前直播上線啦" aria-label="Permalink to “JSDC 全台最大 JS 研討會前直播上線啦！”">&#8203;</a></h1>
<p>先感謝 JSDC 團隊邀請，這次的直播企劃真的是超快閃，週一晚上接到邀請，週二花了一個小時喬 rundown，然後週三晚上就直接上了，幾乎是沒什麼準備的機會，超刺激。還好直播中沒有什麼太大的意外，也謝謝來自各方的觀眾願意來聽我這個大叔練肖威。</p>
<p>這次直播主要就是做個簡單的訪談，跟大家聊聊出社會這些年來是怎麼踏入前端領域的過程。
<del>順便宣傳一下我今年在 JSDC 分享的議程主題這樣。</del></p>
<p>JSDC 報名傳送門： <a href="https://jsdc-tw.kktix.cc/events/jsdc2017" target="_blank" rel="noreferrer">https://jsdc-tw.kktix.cc/events/jsdc2017</a></p>
<p>可能中間有些胡言亂語的部分，就請大家多包涵。</p>
<hr>
<p>這裏就透過這篇文章簡單做個紀錄，也補充在直播沒提到的東西。</p>
<img src="/images/notes/jsdc-live01.png">
<h3 id="一整個鬧到不行的宣傳圖" tabindex="-1">一整個鬧到不行的宣傳圖 <a class="header-anchor" href="#一整個鬧到不行的宣傳圖" aria-label="Permalink to “一整個鬧到不行的宣傳圖”">&#8203;</a></h3>
<h3 id="那些在直播裡面「加油好嗎」的屁話我就不在這邊紀錄了-xd" tabindex="-1">那些在直播裡面「加油好嗎」的屁話我就不在這邊紀錄了 XD <a class="header-anchor" href="#那些在直播裡面「加油好嗎」的屁話我就不在這邊紀錄了-xd" aria-label="Permalink to “那些在直播裡面「加油好嗎」的屁話我就不在這邊紀錄了 XD”">&#8203;</a></h3>
<p>直播實錄</p>
<iframe src="https://www.facebook.com/plugins/video.php?href=https%3A%2F%2Fwww.facebook.com%2FJSDC.TW%2Fvideos%2F1229118017193709%2F&show_text=0&width=560" width="560" height="315" style="border:none;overflow:hidden" scrolling="no" frameborder="0" allowTransparency="true" allowFullScreen="true"></iframe>
<hr>
<blockquote>
<p>什麼樣的契機讓你開始寫程式</p>
</blockquote>
<p>我其實從小學還在 Dos 時代就開始接觸電腦了，早期其實也不知道程式什麼的，都是為了打電動，頂多就是改改 autoexec.bat / config.sys 之類的設定檔，一直到五、六年級的 windows 95 時期才首次接觸網頁。 印象中那個時候很流行「烘培雞」 (HomePage) 什麼的，老師都喜歡教你 <code>&lt;h1&gt;</code> 就是標題， <code>&lt;center&gt;</code> 就是置中，還有一大堆的 <code>&lt;font&gt;</code> 標籤，以及從「網頁建置百寶箱」裡面的各種神秘代碼貼來的特效。</p>
<p>那個時期別說 CSS，連 JavaScript 是什麼都沒聽過。 XD</p>
<p>真正開始寫程式的時候，反而是高中時期參加電研社才首次接觸 C 語言、 VB 之類的。
印象很深刻的是當時還用 Turbo C 寫了一個貪食蛇的小遊戲，現在都忘光了其實 XD</p>
<p>到了高二，雖然讀的是文組，仍因緣際會加入了某個神秘組織「<a href="https://www.ptt.cc/man/CKSHCNCA/M.1281545012.A.D0A.html" target="_blank" rel="noreferrer">成功高中校園網路策進會</a>」 (CKSHCNCA)，一直沒脫離電資領域。 那時幾乎天天中午都在玩 FreeBSD (打混的時候其實都在玩 BBS XD)，也是從那個時候開始接觸了 windows 以外的作業系統，來到另一個世界。</p>
<p>真正開始寫網頁系統，反而是在大學時期學 PHP、ASP.NET C# WebForm 等等，也靠寫網頁賺了一點錢，當然畢業後也就繼續相關的工作。</p>
<hr>
<blockquote>
<p>一直以來都是做前端工作的嗎</p>
</blockquote>
<p>在我剛畢業出來工作的時候，那時業界根本沒有像現在這樣分什麼前端後端的，工作對外都是戲稱「寫網頁的」。
在成為專職的前端工程師以前，早期都是以 ASP、PHP 等後端語言為我工作上的主要技能。</p>
<p>第一份工作是網站開發，當年任職的是人數不多的小公司，沒有細分前後端的編制，所以所有東西都得一條龍自己硬上。
那時候所謂的網頁開發，也就只是寫 HTML 標籤，CSS 還不太會，反正 table 排版無敵，要改顏色、字型就加 font 屬性...
JavaScript ? 喔，那是拿來寫表單驗証、浮動廣告的，網路上範例抄一下頂多加個 alert 就很迷人了。</p>
<p>什麼 JavaScript 啊，CSS 的也是從出來工作之後才自己惡補，也差不多是從那時候才開始接觸 jQuery、Dojo 這樣的前端函式庫工具，距離現在應該也快十年了。</p>
<p>真正知道「前端工程」這個詞，其實是在 2010 年的事了。</p>
<p>時間來到了 2010 年的 OSDC 前夕。</p>
<p>得知 Douglas Crockford (JavaScript: The Good Parts 作者，JSON 的老爸) 受邀來台演講，透過關係知道他在 Yahoo 有內部分享，於是就很厚臉皮地當了一日訪客進去聽演講，順便參觀當時還在古亭的 Yahoo 奇摩。 坦白說，當天印象讓我最深刻的其實不是 Douglas 的演講內容。</p>
<p>除了整場分享以全英語交談，能聽懂的部分有限是原因之一以外，讓我最驚訝的是，原來在網頁開發中，前端工程的領域比我原先的認知還要來得複雜許多。而當時 Yahoo 奇摩甚至有數十位工程師專門負責前端的部分。 當下聽著他們的熱烈討論，除了顛覆我原本對網頁開發的錯誤觀念外，更引爆了我潛藏已久的前端魂，原來網頁前端技術是如此深奧且迷人，然後就此展開了我的大前端之路... XD</p>
<p>不愧人家說天下武功出少林，天下前端出雅虎。同時也差不多是從那開始才積極參加社群，技能點開始往前端的方向走去。</p>
<hr>
<blockquote>
<p>工作職涯方向</p>
</blockquote>
<p>工程師的職涯方向可以有很多不同的發展，不管是走管理的、往技術繼續鑽研的都可以是目標，甚至有技術背景的 PM 也都會是很搶手的選項。
隨著工作經驗的成長，看技術的眼光也會有所不同。 從早期只要完成交辦工作為主要目標，到幾年後，你也許會經歷到需要在各種技術中做選擇，這時候你會有開始有權力可以挑選你想要的工具、技術了，但記得權力伴隨著責任，挑選技術的時候，你要考慮到產品與技術是否適合的場景、學習的成本、維護的成本等等。 甚至是產品的整體系統架構，你所選擇的技術棧 (technology stack) 是不是可以再優化，然後增加產品的轉換率等等的。</p>
<p>這些其實對工程師來說都是很棒的挑戰，也是未來可以思考的方向。</p>
<hr>
<blockquote>
<p>有沒有想要跟後輩說什麼，讓他們少走一些冤枉路</p>
</blockquote>
<p>看到這個問題，如果是年輕一輩的朋友，其實我認為沒有冤枉路，反而很贊成多繞點路，未來的選擇更多。
很多人其實不知道自己適合什麼，也許他看到前端產業好像很夯，就一窩蜂想進入這個行業，但搞不好更適合後端也不一定。
從另一個角度來看，有了後端經驗的前端工程師，比起設計轉職的前端，更能明白後端的困境，以及相同的溝通水準，不會有各說各話的感覺。</p>
<p>我認為保持開放的心，有時候多繞一下對人生職涯來說，也許還反而是件好事。</p>
<p>但如果有心往前端領域走的話，好好把 HTML / CSS / JavaScript 的基本功練好不會吃虧的。
即使是 oo.js / xx.js 它的本質也還是 JavaScript 啊，把基本功練好，未來遇到新技術新工具的時候，至少你不會慌。</p>
<hr>
<blockquote>
<p>避免惰性的好方法</p>
</blockquote>
<h3 id="tdd-talk-driven-development" tabindex="-1">TDD: Talk-Driven Development <a class="header-anchor" href="#tdd-talk-driven-development" aria-label="Permalink to “TDD: Talk-Driven Development”">&#8203;</a></h3>
<p>我認為人都有惰性，我也有 XD</p>
<p>今天當你剛學會一門新技術，想辦法練到可以去跟別人分享甚至協助解決他人的問題的程度時候，你就可以算是掌握了這門技術。
最好的方式就是去投稿吧，去分享吧，屢試不爽。 而且有了時間的死線之後，你就會開始逼自己去整理，去內化這些資訊，好處多多。</p>
<p>而且不要怕講錯，講錯頂多被人糾正，但你卻因此得到了正確的資訊，學到東西就是你的。</p>
<hr>
<blockquote>
<p>對於畢業新鮮人/轉換跑道有沒有什麼建議</p>
</blockquote>
<p>履歷的累積最重要，剛畢業的時候你還能靠學歷，但過幾年之後根本不會有人在意你是哪個學校畢業的。
去找個你覺得可以學到東西的地方，或是有機會碰到新技術的地方，然後好好累積工作經歷。</p>
<p>有個自我檢視的重點是，當你今天在這份工作無法為你的履歷多寫些什麼的時候，就是可以考慮轉職的時候了。
當你繼續在某個地方工作，改變的只是工作的年份，而不是寫下你參與了某某專案，或是導入了什麼技術、為了公司完成了什麼任務的時候，再繼續待下去都是浪費人生。 因為這份工作的經歷無法為你的未來加分，只會繼續消耗人生，這很現實。</p>
<hr>
<blockquote>
<p>跟 JSDC 之間淵源</p>
</blockquote>
<p>一切都是為了「搶票」。 XD
印象還很清楚，當年 2012 首屆 JSDC 票價還很便宜的時候，那時候的研討會幾乎是一開放報名就會秒殺的程度。
主辦單位為了鼓勵大家投稿，就說「投稿者無論有無選上，都有優先購票權」</p>
<p>然後我就投稿了。然後我就上了。
不僅不用搶票，連買票都可以省下來。</p>
<p>怎麼都想不到人生中的第一場上台分享就在中研院，2012 年。 緊張到靠北。</p>
<p>但也因此認識了很多社群前輩，以及 TDD (Talk-Driven Development) 學習法，可以說是利大於弊，哈。</p>
<hr>
<blockquote>
<p>為什麼想要講今年這個主題</p>
</blockquote>
<p>終於回到 JSDC 2017 主題本身了，經過上面的訪談其實可以知道，在這短短幾年內，前端技術領域的發展可以說是一日千里，
而我最近持續在推廣的 VueJS 在這一兩年也有著持續地成長。</p>
<p>凡是存在必有它的意義。 從早期的 jQuery 到後來 Backbone、Angular JS (ng1)、React、 ng2 到我將為各位分享的 VueJS 等工具，其實可以發現某個語言、工具為什麼會受歡迎，很大的原因是它在那個時間點，解決了大多數人遇到的問題，或是在這個領域帶來了什麼新的概念，新的觀點，讓更多人得到啟發。</p>
<p>回到主題。 VueJS 從早期發展至今，也經歷多許多變革與更新。這次我在 JSDC 的分享，主要會由 VueJS 出發，除了針對 VueJS 新特性的介紹外，也簡單分析現代前端框架與前端開發生態圈的變化與演進。 😃</p>
<img src="/images/notes/jsdc-live02.png">
<p>最後再次感謝 JSDC 團隊邀請，讓我有機會擔任一日 <del>seafood</del> 網紅，感謝今天活動主持人 Ali，果然有正妹加持就是收視保證，也感謝只有聲音沒有人影的藏鏡人 Caesar 協助。</p>
<p>最後，也歡迎對 JavaScript 領域的朋友可以持續關注 JSDC 的各項活動，沒報名的朋友現在還來得及報名歐 XD</p>
<p>傳送門： <a href="https://jsdc-tw.kktix.cc/events/jsdc2017" target="_blank" rel="noreferrer">https://jsdc-tw.kktix.cc/events/jsdc2017</a></p>
<p>期待今年的 JSDC 可以與各位朋友、前輩有交流的機會囉，我們 JSDC 見 😃</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>JSDC</category>
            <category>前端</category>
            <category>直播</category>
        </item>
        <item>
            <title><![CDATA[在 Vue 取得 jsonp 的方式]]></title>
            <link>https://kurohsu.dev/notes/在-Vue-Cli-取得-jsonp-的方式.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/在-Vue-Cli-取得-jsonp-的方式.html</guid>
            <pubDate>Thu, 03 Aug 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="在-vue-取得-jsonp-的方式" tabindex="-1">在 Vue 取得 jsonp 的方式 <a class="header-anchor" href="#在-vue-取得-jsonp-的方式" aria-label="Permalink to “在 Vue 取得 jsonp 的方式”">&#8203;</a></h1>
<p>如果你是從 V1 就開始用 Vue 開發的朋友，一定知道 Vue.js 重要的核心特性就是只關注於 view layout 的呈現與 Components 系統，提供最小化且必要的功能給開發者。
其他的功能都可以自由選用第三方套件來完成，這也是為什麼被稱為「漸進式框架」的原因。</p>
<p>所以，如果我們用 Vue.js 來開發網站，且想使用 ajax 從遠端取得資源的時候，也許有些人會選用 jQuery ($.get / $.ajax ...等) ，也有些人會用 vue-resource 來做搭配。 早期官方推薦 <a href="https://github.com/pagekit/vue-resource" target="_blank" rel="noreferrer">vue-resource</a>，到了 V2 官方的推薦 lib 改為 <a href="https://github.com/mzabriskie/axios" target="_blank" rel="noreferrer">Axios</a> 或是直接用<a href="https://developer.mozilla.org/zh-TW/docs/Web/API/Fetch_API" target="_blank" rel="noreferrer">原生 Fetch API</a>。</p>
<p>不過很可惜，除了<del>包山包海</del>的 jQuery 之外，其他目前都還不支援 jsonp 這樣的做法。 也就是說，如果你的專案上需要用到 jsonp ，而且又不希望掛上一大包的 jQuery，這裡有個簡單的套件可以幫助你。 (順便自己筆記)</p>
<hr>
]]></description>
            <content:encoded><![CDATA[<h1 id="在-vue-取得-jsonp-的方式" tabindex="-1">在 Vue 取得 jsonp 的方式 <a class="header-anchor" href="#在-vue-取得-jsonp-的方式" aria-label="Permalink to “在 Vue 取得 jsonp 的方式”">&#8203;</a></h1>
<p>如果你是從 V1 就開始用 Vue 開發的朋友，一定知道 Vue.js 重要的核心特性就是只關注於 view layout 的呈現與 Components 系統，提供最小化且必要的功能給開發者。
其他的功能都可以自由選用第三方套件來完成，這也是為什麼被稱為「漸進式框架」的原因。</p>
<p>所以，如果我們用 Vue.js 來開發網站，且想使用 ajax 從遠端取得資源的時候，也許有些人會選用 jQuery ($.get / $.ajax ...等) ，也有些人會用 vue-resource 來做搭配。 早期官方推薦 <a href="https://github.com/pagekit/vue-resource" target="_blank" rel="noreferrer">vue-resource</a>，到了 V2 官方的推薦 lib 改為 <a href="https://github.com/mzabriskie/axios" target="_blank" rel="noreferrer">Axios</a> 或是直接用<a href="https://developer.mozilla.org/zh-TW/docs/Web/API/Fetch_API" target="_blank" rel="noreferrer">原生 Fetch API</a>。</p>
<p>不過很可惜，除了<del>包山包海</del>的 jQuery 之外，其他目前都還不支援 jsonp 這樣的做法。 也就是說，如果你的專案上需要用到 jsonp ，而且又不希望掛上一大包的 jQuery，這裡有個簡單的套件可以幫助你。 (順便自己筆記)</p>
<hr>
<hr>
<p>首先先安裝 jsonp 這個套件。</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>$ npm install jsonp --save</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>或</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>$ yarn add jsonp</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>然後在你的程式碼內這樣使用：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> jsonp </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'jsonp'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">jsonp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'http://www.example.com/foo'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, { </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">/* DATA HERE */</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">err</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (err) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(err.message);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(data);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><p>就可以了。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
            <category>jsonp</category>
        </item>
        <item>
            <title><![CDATA[從 Vue 來看 CSS 管理方案的發展]]></title>
            <link>https://kurohsu.dev/notes/從Vue來看CSS管理方案的發展.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/從Vue來看CSS管理方案的發展.html</guid>
            <pubDate>Wed, 26 Jul 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="從-vue-來看-css-管理方案的發展" tabindex="-1">從 Vue 來看 CSS 管理方案的發展 <a class="header-anchor" href="#從-vue-來看-css-管理方案的發展" aria-label="Permalink to “從 Vue 來看 CSS 管理方案的發展”">&#8203;</a></h1>
]]></description>
            <content:encoded><![CDATA[<h1 id="從-vue-來看-css-管理方案的發展" tabindex="-1">從 Vue 來看 CSS 管理方案的發展 <a class="header-anchor" href="#從-vue-來看-css-管理方案的發展" aria-label="Permalink to “從 Vue 來看 CSS 管理方案的發展”">&#8203;</a></h1>
<hr>
<p>昨天看到 caesar 大大發表的 <a href="https://blog.caesarchi.com/2017/07/25/react-css-styled-components/" target="_blank" rel="noreferrer">react &amp; CSS 另類新選擇</a>，講的其實是 styled-components + react 的 CSS 處理方案。 現今的幾個主流前端框架大多也有類似的做法，身為 Vue 的擁護者，這裡就來簡單說明一下 Vue 的處理方式。</p>
<p>先從早期的 CSS 管理方案開始說起吧。</p>
<h2 id="css-預處理器、css-命名與架構學" tabindex="-1">CSS 預處理器、CSS 命名與架構學 <a class="header-anchor" href="#css-預處理器、css-命名與架構學" aria-label="Permalink to “CSS 預處理器、CSS 命名與架構學”">&#8203;</a></h2>
<p>這麼多年來，CSS 的管理一直都是開發者的夢靨，很大的原因在於 CSS 的程式化與 JavaScript 相比其實是相對困難的，
尤其在於 JS 至少還有它的 scope 可以切分，而 CSS 在這點是相對弱勢的，所有的 CSS 樣式都是 global scoped。 如果頁面上的模組過多，管理起來更是難以維護。
嚴格來說，CSS 本身甚至都不能算是個程式語言。</p>
<p>所以，早期在程式面會有所謂的 LESS, SASS, Stylus 等這樣的工具，藉由 preprocessor (預處理器) 來做編譯，可以做到繼承、重用、複寫等功能。
另一方面，除了上述說的透過工具的預先編譯外，也有另一派人馬提倡的是，由 CSS 的命名學 / 架構論 來完成 CSS 模組的管理與複用。</p>
<p>在幾年前 (2014) ，小弟也有針對此類 CSS 方法論在前端社群聚會做分享，有興趣的朋友可以看下面這份投影片：</p>
<iframe src="//www.slideshare.net/slideshow/embed_code/key/MOVEP2qBeaGyEO" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> 
<p>簡單來說，就是透過命名規則，來做到 CSS 架構的管理。
<del>但大家都知道，訂出了 convention ，要是沒有嚴格檢查是否遵守，其實跟沒有一樣 XD </del></p>
<h2 id="css-modules" tabindex="-1">CSS Modules <a class="header-anchor" href="#css-modules" aria-label="Permalink to “CSS Modules”">&#8203;</a></h2>
<p>所以後來出現了比較符合人性的 CSS Modules，透過工具來處理先前那些人工命名規範要做的事。
也就是說，由 Webpack 做了 BEM 的事情。 過去 BEM 是人工手動做，而 Webpack 是交給工具自動化做，用類似 Javascript module 的方式來處理 CSS。</p>
<p>講了大半個篇幅，終於要講到目前的主流： CSS in JS / styled-components 了。</p>
<h2 id="css-in-js-all-in-js" tabindex="-1">CSS in JS / All in JS <a class="header-anchor" href="#css-in-js-all-in-js" aria-label="Permalink to “CSS in JS / All in JS”">&#8203;</a></h2>
<p>CSS in JS 的概念最早是由 React 社群所提出，支持與不支持的兩派在社群當中也有著激烈的論戰。
我過去也是反對此類做法，但目前保持中立態度看待。 XD</p>
<p>過去我剛入行的時候，前輩大力倡導 HTML / CSS / JavaScript 三者負責的領域要切得越乾淨越好，簡單來說，inline-style 與 inline-script 都是禁止的。
一旦 inline-style 與 inline-script 寫得越多，程式碼就會像義大利麵般攪在一起難以維護。</p>
<p>而自從 component-based 的前端工具出現後，過去的原則漸漸被打破。
像 React ，就有下面這樣的寫法。</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> style</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'color'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'red'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'fontSize'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'46px'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> clickHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> alert</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'hi'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">); </span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">ReactDOM.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">render</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> style</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{style} </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">onclick</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{clickHandler}></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">     Hello, world!</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  document.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getElementById</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br></div></div><p>將一個網頁元件 (H1) 的<strong>樣式、事件與結構，通通封裝在一份程式碼</strong>當中。</p>
<p>而 Vue 也有類似的做法，像 Vue 提供的 Vue 元件檔：</p>
<div class="language-HTML line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">HTML</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">/* HTML */</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> @click</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"clickHandler"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ msg }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">/* script */</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">module</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      msg: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Hello, world!'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  methods:{</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    clickHandler</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">      alert</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'hi'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">/* scoped CSS */</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">style</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> scoped</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">red</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  font-size</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">46</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">px</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br></div></div><p>在 Vue 元件檔，透過上面這樣的方式提供了一個 template (HTML)、script 以及帶有 scoped 的 style 樣式，也仍然可以保有過去 HTML、CSS 與 JS 分離的開發體驗。
但本質上仍是 all-in-JS 的變種語法糖就是了 XD</p>
<img src="/images/notes/vue-file.png">
<p>值得一提的是，當 style 標籤加上 <strong>scoped</strong> 屬性，那麼在 Vue 元件檔經過編譯後，會自動在元件上打上一個隨機的屬性，再透過 CSS Attribute Selector 的特性來做到 CSS scope 的切分，使得即便是在不同元件檔裡的 h1 也能有 CSS 樣式不互相干擾的效果。 當然開發起來，比起 JSX、或是 inline-style 等的方式，這種折衷的作法更合我的胃口 😁</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
            <category>CSS</category>
        </item>
        <item>
            <title><![CDATA[不需編譯也能載入 .vue 元件檔: 使用 http-vue-loader]]></title>
            <link>https://kurohsu.dev/notes/不需編譯也能載入-vue-元件檔-使用-http-vue-loader.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/不需編譯也能載入-vue-元件檔-使用-http-vue-loader.html</guid>
            <pubDate>Mon, 10 Jul 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="不需編譯也能載入-vue-元件檔-使用-http-vue-loader" tabindex="-1">不需編譯也能載入 .vue 元件檔: 使用 http-vue-loader <a class="header-anchor" href="#不需編譯也能載入-vue-元件檔-使用-http-vue-loader" aria-label="Permalink to “不需編譯也能載入 .vue 元件檔: 使用 http-vue-loader”">&#8203;</a></h1>
<p>上週在 Vue 社群圈有個令人興奮的熱門新聞: CodePen 可以支援 <code>.vue</code> 檔案了！</p>
<blockquote class="twitter-tweet" data-lang="zh-tw"><p lang="en" dir="ltr">Check it out - you can use `.vue` files in CodePen Projects easily:<a href="https://t.co/Cd3Qr11xYQ">https://t.co/Cd3Qr11xYQ</a> <a href="https://t.co/QvtIXxKRUk">pic.twitter.com/QvtIXxKRUk</a></p>&mdash; Chris Coyier (@chriscoyier) <a href="https://twitter.com/chriscoyier/status/880859500185616387">2017年6月30日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>身為使用 Vue 的開發者聽到這樣的消息當然感到相當開心，但同時也不禁感到好奇，CodePen 是如何做到不須透過 webpack 編譯 vue 檔案，就可以將 .vue 的 component 如實顯示到網頁中。</p>
<p>大家都知道，在網頁開發的世界中，前端是沒有秘密的。 打開了開發工具，才知道原來是透過 <strong><a href="https://github.com/FranckFreiburger/http-vue-loader" target="_blank" rel="noreferrer">http-vue-loader</a></strong> 這個工具做到的。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="不需編譯也能載入-vue-元件檔-使用-http-vue-loader" tabindex="-1">不需編譯也能載入 .vue 元件檔: 使用 http-vue-loader <a class="header-anchor" href="#不需編譯也能載入-vue-元件檔-使用-http-vue-loader" aria-label="Permalink to “不需編譯也能載入 .vue 元件檔: 使用 http-vue-loader”">&#8203;</a></h1>
<p>上週在 Vue 社群圈有個令人興奮的熱門新聞: CodePen 可以支援 <code>.vue</code> 檔案了！</p>
<blockquote class="twitter-tweet" data-lang="zh-tw"><p lang="en" dir="ltr">Check it out - you can use `.vue` files in CodePen Projects easily:<a href="https://t.co/Cd3Qr11xYQ">https://t.co/Cd3Qr11xYQ</a> <a href="https://t.co/QvtIXxKRUk">pic.twitter.com/QvtIXxKRUk</a></p>&mdash; Chris Coyier (@chriscoyier) <a href="https://twitter.com/chriscoyier/status/880859500185616387">2017年6月30日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>身為使用 Vue 的開發者聽到這樣的消息當然感到相當開心，但同時也不禁感到好奇，CodePen 是如何做到不須透過 webpack 編譯 vue 檔案，就可以將 .vue 的 component 如實顯示到網頁中。</p>
<p>大家都知道，在網頁開發的世界中，前端是沒有秘密的。 打開了開發工具，才知道原來是透過 <strong><a href="https://github.com/FranckFreiburger/http-vue-loader" target="_blank" rel="noreferrer">http-vue-loader</a></strong> 這個工具做到的。</p>
<hr>
<hr>
<p><strong><a href="https://github.com/FranckFreiburger/http-vue-loader" target="_blank" rel="noreferrer">http-vue-loader</a></strong> 這套工具可提供開發者直接在網頁環境中載入 <code>.Vue</code> File，無需透過 nodeJS 環境編譯，也不需要 Build 的步驟。</p>
<p>用法很簡單，首先在網頁上載入 Vue 與 http-vue-loader，如下</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://unpkg.com/vue"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://unpkg.com/http-vue-loader"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p>接著，假設我們有一個 <code>my-component.vue</code> 的檔案：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"hello"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>Hello {{who}}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">module</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            who: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'world'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.hello</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    background-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">#ffe</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br></div></div><p>那麼，我們就可以在 <code>components</code> 內透過 <code>httpVueLoader</code> 來載入我們的子元件：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>&#x3C;div id="my-app"></span></span>
<span class="line"><span>  &#x3C;my-component>&#x3C;/my-component></span></span>
<span class="line"><span>&#x3C;/div></span></span>
<span class="line"><span></span></span>
<span class="line"><span>&#x3C;script type="text/javascript"></span></span>
<span class="line"><span>  new Vue({</span></span>
<span class="line"><span>    el: '#my-app',</span></span>
<span class="line"><span>    components: {</span></span>
<span class="line"><span>      'my-component': httpVueLoader('my-component.vue')</span></span>
<span class="line"><span>    }</span></span>
<span class="line"><span>  });</span></span>
<span class="line"><span>&#x3C;/script></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><br>
<p>當然，httpVueLoader 也提供了類似 <code>Vue.component('my-component', { ... })</code> 的宣告方式:</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  httpVueLoaderRegister</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Vue, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'my-component.vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      components: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">          'my-component'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>或是將 httpVueLoader 當作 Plugin 來使用：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  Vue.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">use</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(httpVueLoader);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    components: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        'my-component'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'url:my-component.vue'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    ...</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><p>甚至是 Array 的形式也可以：</p>
<div class="language-js line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        components: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            'url:my-component.vue'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        ...</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>需要注意的是，httpVueLoader 目前僅支援 Vue 2 以上的版本，而作者也在專案內說明， httpVueLoader 只是提供一個簡便的測試與開發環境，方便開發者不需要透過繁複的編譯過程才能使用 vue file 進行開發。</p>
<p>若是要發佈到線上的專案，建議還是需要透過工具編譯打包，會有更好的效能以及更佳的支援度喔。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
            <category>httpVueLoader</category>
        </item>
        <item>
            <title><![CDATA[如何在 Vue-CLI 建立的開發環境呼叫跨域遠端 RESTful APIs]]></title>
            <link>https://kurohsu.dev/notes/如何在-Vue-CLI-建立的開發環境呼叫跨域遠端-RESTful-APIs.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/如何在-Vue-CLI-建立的開發環境呼叫跨域遠端-RESTful-APIs.html</guid>
            <pubDate>Wed, 07 Jun 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="如何在-vue-cli-建立的開發環境呼叫跨域遠端-restful-apis" tabindex="-1">如何在 Vue-CLI 建立的開發環境呼叫跨域遠端 RESTful APIs <a class="header-anchor" href="#如何在-vue-cli-建立的開發環境呼叫跨域遠端-restful-apis" aria-label="Permalink to “如何在 Vue-CLI 建立的開發環境呼叫跨域遠端 RESTful APIs”">&#8203;</a></h1>
<p>前幾天在 VueTW 活動結束後，有朋友來問：Vue 的開發環境能不能在 local 端建立 proxy 服務來解決跨域讀取後端的 API，
<del>隔壁棚的 Angular CLI 好像有這功能</del>，<a href="https://github.com/vuejs/vue-cli" target="_blank" rel="noreferrer">Vue-CLI</a> 是不是也能做到。</p>
<p>(使用 Angular 開發專案的朋友，這裏推薦參考 Will 保哥這篇文章：<a href="http://blog.miniasp.com/post/2017/02/05/Setup-proxy-to-backend-in-Angular-CLI.aspx" target="_blank" rel="noreferrer">如何在 Angular CLI 建立的 Angular 2 開發環境呼叫遠端 RESTful APIs</a>)。</p>
<p>使用 Vue 的朋友，別著急莫緊張，Vue-CLI 也有提供類似功能喔。</p>
<blockquote>
<p>注意： 本文撰寫時，使用 Node v8.0.0 / NPM 5.0.0 與 yarn v0.24.6，Vue-CLI 為 2.8.2。</p>
</blockquote>
]]></description>
            <content:encoded><![CDATA[<h1 id="如何在-vue-cli-建立的開發環境呼叫跨域遠端-restful-apis" tabindex="-1">如何在 Vue-CLI 建立的開發環境呼叫跨域遠端 RESTful APIs <a class="header-anchor" href="#如何在-vue-cli-建立的開發環境呼叫跨域遠端-restful-apis" aria-label="Permalink to “如何在 Vue-CLI 建立的開發環境呼叫跨域遠端 RESTful APIs”">&#8203;</a></h1>
<p>前幾天在 VueTW 活動結束後，有朋友來問：Vue 的開發環境能不能在 local 端建立 proxy 服務來解決跨域讀取後端的 API，
<del>隔壁棚的 Angular CLI 好像有這功能</del>，<a href="https://github.com/vuejs/vue-cli" target="_blank" rel="noreferrer">Vue-CLI</a> 是不是也能做到。</p>
<p>(使用 Angular 開發專案的朋友，這裏推薦參考 Will 保哥這篇文章：<a href="http://blog.miniasp.com/post/2017/02/05/Setup-proxy-to-backend-in-Angular-CLI.aspx" target="_blank" rel="noreferrer">如何在 Angular CLI 建立的 Angular 2 開發環境呼叫遠端 RESTful APIs</a>)。</p>
<p>使用 Vue 的朋友，別著急莫緊張，Vue-CLI 也有提供類似功能喔。</p>
<blockquote>
<p>注意： 本文撰寫時，使用 Node v8.0.0 / NPM 5.0.0 與 yarn v0.24.6，Vue-CLI 為 2.8.2。</p>
</blockquote>
<hr>
<blockquote>
<p>實際執行情況可能因執行環境的版本會有所差異。</p>
</blockquote>
<p>首先，第一步當然是先安裝 Vue-CLI:</p>
<ul>
<li>NPM:</li>
</ul>
<div class="language-shell line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -g</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> vue-cli</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><ul>
<li>YARN:</li>
</ul>
<div class="language-shell line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> yarn</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> global</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> vue-cli</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><hr>
<p>安裝好了之後，接著我們透過 Vue-CLI 來建立新專案，樣板的部分我們選用 <span style="color: red;">webpack</span>，然後執行：</p>
<div class="language-shell line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> $</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> vue</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> init</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> webpack</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> test</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>上面指令中的 <code>test</code> 是專案名稱，這裏請自行取名替換成你要的名稱。 為了節省時間，範例裡只安裝必要的套件，其他選項可依你的需要選用安裝。
<img src="/images/notes/vue-cli.png"></p>
<p>接著切換至新增好的目錄後，安裝相關套件。</p>
<ul>
<li>NPM:</li>
</ul>
<div class="language-shell line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> cd</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> test</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> i</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> run</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> dev</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><ul>
<li>YARN:</li>
</ul>
<div class="language-shell line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> cd</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> test</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> yarn</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> yarn</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> dev</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>安裝完成並執行 dev-server 之後，應該會自動開啟瀏覽器 <code>http://localhost:8080/</code> 並看到畫面：
<img src="/images/notes/hello-vue.png"></p>
<p>到目前為止已經建立起一個基本的 Vue 專案了。</p>
<hr>
<p>接著，我們以 <a href="http://data.taipei/" target="_blank" rel="noreferrer">Data.Taipei</a> 的 <a href="http://data.taipei/opendata/datalist/datasetMeta/preview?id=8ef1626a-892a-4218-8344-f7ac46e1aa48&amp;rid=9c6a96d6-353c-41c0-84cc-d181988304f2" target="_blank" rel="noreferrer">Youbike臺北市公共自行車即時資訊</a> 作為本次的範例。
<img src="/images/notes/data-taipei-Youbike.png"></p>
<p>VueJS 的核心並沒有提供 Ajax 這樣的功能，所以在發送遠端請求的時候，我們要先安裝 <a href="https://github.com/mzabriskie/axios" target="_blank" rel="noreferrer">axios</a> 這樣的套件來協助我們，當然你想要用 jQuery AJAX 或是 Fetch API 也是 ok 的。</p>
<p>安裝方式：</p>
<ul>
<li>NPM:</li>
</ul>
<div class="language-shell line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --save</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> axios</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> vue-axios</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><ul>
<li>YARN:</li>
</ul>
<div class="language-shell line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> yarn</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> axios</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> vue-axios</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>安裝完畢後，我們打開 App.vue 檔，並加上 <code>import</code> 以及 <code>Vue.use</code> 來載入外部的套件：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Vue </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> axios </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'axios'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> VueAxios </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue-axios'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Vue.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">use</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(VueAxios, axios)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>接著，我們在 Vue 實體的 <code>create</code> hook 裡面來送出 GET 請求試試：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  created</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    Vue.axios.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'http://data.taipei/youbike'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">then</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">response</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(response.data)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>像這樣：
<img src="/images/notes/vue-axios-code.png"></p>
<p>存檔後再次開啟 dev-server，毫無意外地，你應該會看到像這樣的錯誤：
<img src="/images/notes/ajax-error.png"></p>
<p>因為遠端的 API 並沒有開啟 CORS 協定，所以我們無法跨域存取這個資源。 不過還好，Vue-CLI 的 dev server 整合了 <a href="https://github.com/chimurai/http-proxy-middleware" target="_blank" rel="noreferrer">http-proxy-middleware</a>。 這個工具替我們在 local 端建立一個代理層，讓我們在開發時期就可以很方便的呼叫遠端的 HTTP API。</p>
<p>套件的部分，Vue-CLI 都幫我們準備好了，接著打開專案下 <code>config/index.js</code> 這個檔案，然後編輯 <code>proxyTable: {}</code> 的部分。
<img src="/images/notes/proxyTable.png"></p>
<p>修改 <code>config/index.js</code> 如下：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  proxyTable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    '/data'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {                        </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 自訂 local 端的位置</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      target: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'http://data.taipei/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 遠端 URL Domain</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      changeOrigin: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      pathRewrite: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        '^/data'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><p>經過這樣設定後，<code>http://data.taipei/</code> 網域內的資源，都會在 local 端以 <code>/data</code> 的形式被代理，也就是說，像 <code>http://data.taipei/youbike</code> 這樣的遠端資源，我們就可以在 local 端用 <code>/data/youbike</code> 來取得。</p>
<p>所以，再回到 <code>App.vue</code>，這裏將原本的 <code>http://data.taipei/youbike</code> 改成 <code>/data/youbike</code>，像這樣：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  created</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    Vue.axios.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'/data/youbike'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">then</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">response</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(response.data)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>接著，記得要重開 dev-server，執行 <code>$ npm run dev</code> 或 <code>$ yarn dev</code> 。</p>
<p>最後，在網頁讀取完成後，打開 console 就可以看到，遠端的 youbike 資訊已經可以正確取得囉。
<img src="/images/notes/vue-axios-ok.png"></p>
<p>詳細的 http-proxy-middleware 設定可以參考這裡：</p>
<ul>
<li><a href="https://github.com/chimurai/http-proxy-middleware#http-proxy-middleware-options" target="_blank" rel="noreferrer">https://github.com/chimurai/http-proxy-middleware#http-proxy-middleware-options</a></li>
<li><a href="https://vuejs-templates.github.io/webpack/proxy.html" target="_blank" rel="noreferrer">https://vuejs-templates.github.io/webpack/proxy.html</a></li>
</ul>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
        </item>
        <item>
            <title><![CDATA[VueJS-V2 在 v-for 列表完成分頁功能 (從 v1 至 v2)]]></title>
            <link>https://kurohsu.dev/notes/VueJS-V2-在-v-for-列表完成分頁功能.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/VueJS-V2-在-v-for-列表完成分頁功能.html</guid>
            <pubDate>Tue, 30 May 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Vue 2 都發行半年多了，直到最近有網友留言這才想起一直沒更新裡面的內容，囧。</p>
<p>延續上回 <a href="https://kuro.tw/posts/2016/05/30/vuejs-in-v-for-through-the-filter-in-the-list-complete-search-and-page-functions/" target="_blank" rel="noreferrer">[VueJS] 在 v-for 列表中透過 filter 完成搜尋與分頁的功能</a> 這篇的說明，
這次我們來看看，自從 <span style="color: red">VueJS 更新到 V2 拿掉了內建的 filterBy、limitBy 等好用的 filter 後</span>，要如何修改才能做到分頁的功能。</p>
<p>首先看到原本 V1 的範例，是這樣的，這裡透過 <code>limitBy countOfPage pageStart</code> 來完成分頁，如下：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"r in rows </span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    | filterBy filter_name in 'name'</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<p>Vue 2 都發行半年多了，直到最近有網友留言這才想起一直沒更新裡面的內容，囧。</p>
<p>延續上回 <a href="https://kuro.tw/posts/2016/05/30/vuejs-in-v-for-through-the-filter-in-the-list-complete-search-and-page-functions/" target="_blank" rel="noreferrer">[VueJS] 在 v-for 列表中透過 filter 完成搜尋與分頁的功能</a> 這篇的說明，
這次我們來看看，自從 <span style="color: red">VueJS 更新到 V2 拿掉了內建的 filterBy、limitBy 等好用的 filter 後</span>，要如何修改才能做到分頁的功能。</p>
<p>首先看到原本 V1 的範例，是這樣的，這裡透過 <code>limitBy countOfPage pageStart</code> 來完成分頁，如下：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"r in rows </span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    | filterBy filter_name in 'name' </span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    | recordLength 'filteredRowCount' </span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    | limitBy countOfPage pageStart "</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ (currPage-1) * countOfPage + $index + 1 }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.name }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.age }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.gender }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><p>因為從 V2 開始，<code>filterBy</code> 、 <code>limitBy</code> 、 <code>orderBy</code> 都沒有了，所以我們直接移除掉：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"r in rows"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ (currPage-1) * countOfPage + $index + 1 }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.name }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.age }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.gender }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>這時應該會出現錯誤，因為 <code>$index</code> 的寫法也更新了，所以改一下 <code>v-for</code> 的內容：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"(r, index) in rows"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ (currPage-1) * countOfPage + index + 1 }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.name }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.age }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.gender }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>到目前為止，至少可以看到程式正常運作，但相對地<span style="color: red">分頁與搜尋的功能也沒了</span>，所以接下來先來處理<strong>搜尋</strong>的部份。</p>
<hr>
<p>首先，在 <code>computed</code> 屬性，我們新增一個叫做 <code>filteredRows</code> 的屬性，用來處理與 <code>filter_name</code> 比對後的結果：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  filteredRows</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 因為 JavaScript 的 filter 有分大小寫，</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 所以這裡將 filter_name 與 rows[n].name 通通轉小寫方便比對。</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> filter_name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.filter_name.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toLowerCase</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    </span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 如果 filter_name 有內容，回傳過濾後的資料，否則將原本的 rows 回傳。</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.filter_name.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> </span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.rows.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){ </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d.name.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toLowerCase</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">indexOf</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(filter_name) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; }) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> </span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.rows;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><p>然後將 html 內的 <code>rows</code> 換成 <code>filteredRows</code></p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"(r, index) in filteredRows"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ (currPage-1) * countOfPage + index + 1 }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.name }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.age }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.gender }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>name 搜尋的部份到這裡就算完成了，接著回頭來處理<strong>分頁</strong>。</p>
<hr>
<p>處理分頁之前，先看到底下的分頁按鈕， <code>n in range</code> 在 V2 的寫法也有不同，原本的索引會由 0 開始，但從 V2 開始會由 1 開始，所以需要把原本的：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"pagination"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">ul</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-bind:class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"{'disabled': (currPage === 1)}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> </span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        @click.prevent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"setPage(currPage-1)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>Prev&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"n in totalPage"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        v-bind:class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"{'active': (currPage === (n+1))}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> </span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        @click.prevent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"setPage(n+1)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{n+1}}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-bind:class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"{'disabled': (currPage === totalPage)}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> </span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        @click.prevent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"setPage(currPage+1)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>Next&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">ul</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><p>改寫成：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"pagination"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">ul</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-bind:class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"{'disabled': (currPage === 1)}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> </span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        @click.prevent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"setPage(currPage-1)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>Prev&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"n in totalPage"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        v-bind:class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"{'active': (currPage === (n))}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> </span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        @click.prevent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"setPage(n)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{n}}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-bind:class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"{'disabled': (currPage === totalPage)}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> </span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        @click.prevent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"setPage(currPage+1)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>Next&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">ul</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><p>顯示就會是正常的從 1 開始了。</p>
<p>再來處理顯示筆數時，需要的會有 <code>pageStart</code> 、 <code>countOfPage</code> 與 <code>totalPage</code> 這三個部分，其中 <code>totalPage</code> 會跟先前提到的 <strong>搜尋</strong> 比較有關係，因為總頁數不對，算出來的分頁也會有問題。</p>
<p>原本的 <code>totalPage</code> 長這樣：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  totalPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.filter_name.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ceil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.rows.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> /</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.countOfPage);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ceil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.filteredRowCount </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.countOfPage);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>因為資料在先前都交給 <code>filteredRows</code> 處理了，所以可以改寫成</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  totalPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ceil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.filteredRows.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> /</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.countOfPage);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>實際執行會發現最底下的分頁按鈕已經會隨著 Filter 的結果筆數而有所不同：</p>
<img src="/images/notes/v2-filter-1.png">
<hr>
<img src="/images/notes/v2-filter-2.png">
<p>最後，再回到處理<span style="color: red">每頁顯示資料量</span>的部分。</p>
<p>在沒有了 <code>limitBy</code> 之後，顯示筆數的部份我們可以透過 JavaScript 的 <code>slice</code> 來處理，當然也可以寫在 <code>computed</code> 屬性。
但這個範例只是單純在顯示作切換，所以可以直接寫在 view 上，像這樣將 <code>filteredRows</code> 改寫成 <code>filteredRows.slice(pageStart, pageStart + countOfPage)</code>：</p>
<p>對 <code>slice</code> 不熟的朋友可參考： MDN: <a href="https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/slice" target="_blank" rel="noreferrer">Array.prototype.slice()</a></p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"(r, index) in filteredRows.slice(pageStart, pageStart + countOfPage)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ (currPage-1) * countOfPage + index + 1 }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.name }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.age }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>{{ r.gender }}&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>就完成了從 V1 更新到 V2 的過程了。</p>
<p>完整更新的範例可在此參考：<br>
<a class="jsbin-embed" href="http://jsbin.com/kusafiqaka/embed?html,js,output">JS Bin on jsbin.com</a></p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
        </item>
        <item>
            <title><![CDATA[筆記 Google 街景 API 加入圖標與預設角度計算]]></title>
            <link>https://kurohsu.dev/notes/筆記-Google-街景-API-加入圖標與角度計算.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/筆記-Google-街景-API-加入圖標與角度計算.html</guid>
            <pubDate>Tue, 04 Apr 2017 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="筆記-google-街景-api-加入圖標與預設角度計算" tabindex="-1">筆記 Google 街景 API 加入圖標與預設角度計算 <a class="header-anchor" href="#筆記-google-街景-api-加入圖標與預設角度計算" aria-label="Permalink to “筆記 Google 街景 API 加入圖標與預設角度計算”">&#8203;</a></h1>
<p>相信大家都知道 Google Map 在多年以前就開放了「街景檢視」這樣的服務，當然 Google Map API 也提供了給開發者使用相關的 API 服務：「<a target="_blank" href="https://developers.google.com/maps/documentation/javascript/streetview">Street View Service</a>」來開發地圖的應用。</p>
<p>除了原本在 Google Map 上面就有的街景檢視之外，透過 Street View API 開發者可以自由地在街景地圖上加入標示項，作法就跟平時在地圖上加入 marker 一樣：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> marker </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Marker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    map: map,</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<h1 id="筆記-google-街景-api-加入圖標與預設角度計算" tabindex="-1">筆記 Google 街景 API 加入圖標與預設角度計算 <a class="header-anchor" href="#筆記-google-街景-api-加入圖標與預設角度計算" aria-label="Permalink to “筆記 Google 街景 API 加入圖標與預設角度計算”">&#8203;</a></h1>
<p>相信大家都知道 Google Map 在多年以前就開放了「街景檢視」這樣的服務，當然 Google Map API 也提供了給開發者使用相關的 API 服務：「<a target="_blank" href="https://developers.google.com/maps/documentation/javascript/streetview">Street View Service</a>」來開發地圖的應用。</p>
<p>除了原本在 Google Map 上面就有的街景檢視之外，透過 Street View API 開發者可以自由地在街景地圖上加入標示項，作法就跟平時在地圖上加入 marker 一樣：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> marker </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Marker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    map: map,</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    icon: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'//chart.googleapis.com/chart?chst=d_map_pin_icon&#x26;chld=train|ffff00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    position: {lat: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25.051269</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, lng: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">121.512386</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><p>如下圖黃色 icon：
<img src="/images/notes/map-marker.png"></p>
<p>這個時候我們切換成街景檢視的時候，你會發現還需要自己手動調整角度才能找到 icon 的位置：
<br>(如下地圖，可按右下角按鈕切換街景)</p>
<iframe style="border: 1px solid #aaa; width: 720px; height: 300px;" src="https://kuro.tw/demo-pages/maps/map-add-marker.html"></iframe>
<p>需要將視角往左邊移動後才能找到 icon 的位置。</p>
<hr>
<p>那麼，是否有更好的方式可以讓 Google 街景可以自動判斷視角方向呢？</p>
<p>有的，可以利用 Google Map 所提供的 <a target="_blank" href="https://developers.google.com/maps/documentation/javascript/geometry">Geometry Library</a> 來協助我們計算兩點之間的角度。</p>
<p>在 Google Maps API 內，「方向」是以與正北的角度來定義，也就是從正北 (0 度) 的順時針方向計算的角度，可以使用 <code>computeHeading()</code> 方法，將兩個 <code>from</code> 和 <code>to</code> 的 LatLng 物件傳遞給它，來計算兩個位置之間的方向。</p>
<p>如下：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> heading </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.geometry.spherical.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">computeHeading</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(from_latLng, target_latlng);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p><code>google.maps.geometry.spherical.computeHeading( )</code> 就會回傳兩點之間的角度，接著，我們就可以透過
<code>panorama.setPov({ heading: heading, pitch: 0 });</code> 來指定街景的預設角度了。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>Google Map</category>
            <category>google map</category>
            <category>street map api</category>
        </item>
        <item>
            <title><![CDATA[透過 Vue-CLI 建置專案時，自動切換 devtools 的 debug 環境]]></title>
            <link>https://kurohsu.dev/notes/透過-VueCLI-建置專案時，自動切換-devtools-的-debug-環境.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/透過-VueCLI-建置專案時，自動切換-devtools-的-debug-環境.html</guid>
            <pubDate>Thu, 08 Dec 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="透過-vue-cli-建置專案時-自動切換-devtools-的-debug-環境" tabindex="-1">透過 Vue-CLI 建置專案時，自動切換 devtools 的 debug 環境 <a class="header-anchor" href="#透過-vue-cli-建置專案時-自動切換-devtools-的-debug-環境" aria-label="Permalink to “透過 Vue-CLI 建置專案時，自動切換 devtools 的 debug 環境”">&#8203;</a></h1>
<p>Vue 提供了相當好用的 debug 工具 (Chrome 套件)， <a href="https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd" target="_blank" rel="noreferrer">Vue.js devtools</a>，在安裝之後當你用 Chrome 開啟 Vue 專案時，Chrome 的開發者工具會多出一個 Vue 的 Tab，然後把 Vue Instance 裡的屬性通通列出來：</p>
<img src="/images/notes/vue-tool-screenshot.png">
<p>在預設開啟 debug mode 時，按下 Vue devtools 的 icon 你會看到這樣畫面：
<img src="/images/notes/vue-tool-on.png"></p>
<p>這時候打開 Chrome 開發者工具， vue 專案中所有 Instance 的屬性資料通通一覽無遺。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="透過-vue-cli-建置專案時-自動切換-devtools-的-debug-環境" tabindex="-1">透過 Vue-CLI 建置專案時，自動切換 devtools 的 debug 環境 <a class="header-anchor" href="#透過-vue-cli-建置專案時-自動切換-devtools-的-debug-環境" aria-label="Permalink to “透過 Vue-CLI 建置專案時，自動切換 devtools 的 debug 環境”">&#8203;</a></h1>
<p>Vue 提供了相當好用的 debug 工具 (Chrome 套件)， <a href="https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd" target="_blank" rel="noreferrer">Vue.js devtools</a>，在安裝之後當你用 Chrome 開啟 Vue 專案時，Chrome 的開發者工具會多出一個 Vue 的 Tab，然後把 Vue Instance 裡的屬性通通列出來：</p>
<img src="/images/notes/vue-tool-screenshot.png">
<p>在預設開啟 debug mode 時，按下 Vue devtools 的 icon 你會看到這樣畫面：
<img src="/images/notes/vue-tool-on.png"></p>
<p>這時候打開 Chrome 開發者工具， vue 專案中所有 Instance 的屬性資料通通一覽無遺。</p>
<hr>
<hr>
<p>但是當專案完成要上線的時候，就應該要將 debug mode 關閉，尤其在屬性可能會存有較為機敏性資料的時候。
關閉的方式也很簡單，加上這兩行就可以了：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Vue.config.debug </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Vue.config.devtools </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p>關閉後，再開啟 Vue 專案，雖然 icon 偵測到 Vue.js 會亮起來，但是你會看到這樣訊息：
<img src="/images/notes/vue-tool-disabled.png"></p>
<p>這時候 Chrome 開發者工具的 Vue tab 就不會出現了。</p>
<hr>
<p>但是，在建置專案的時候，常常會忘記手動調整 debug mode，導致上線的專案會是開啟 debug mode 的版本，
是否可以在 webpack 啟動的時候自動判斷目前環境呢？</p>
<p>以 Vue-CLI 提供的 package.json 為例，他提供了兩個預設的 scripts， dev 用在開發專案時使用，而 build 則用於建置 production 使用：
<img src="/images/notes/vue-package-json.png"></p>
<p>圖中可以看到，在 build 模式下多了 <code>NODE_ENV=production</code> 這個參數。</p>
<p>這時候我們就可以利用 <code>NODE_ENV</code> 這個參數來判斷使否開啟 debug mode：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Vue </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> App </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './App.vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> isDebug_mode</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> process.env.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">NODE_ENV</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'production'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Vue.config.debug </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> isDebug_mode;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Vue.config.devtools </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> isDebug_mode;</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><p>這樣 <code>Vue.config.debug</code> 的版本就會自動依照執行階段的不同而有不同的設定了。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
            <category>vue-cli</category>
            <category>webpack</category>
            <category>devtools</category>
        </item>
        <item>
            <title><![CDATA[利用 Yarn 安裝 Vue-cli]]></title>
            <link>https://kurohsu.dev/notes/利用-yarn-安裝-vue-cli.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/利用-yarn-安裝-vue-cli.html</guid>
            <pubDate>Thu, 13 Oct 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="利用-yarn-安裝-vue-cli" tabindex="-1">利用 Yarn 安裝 Vue-cli <a class="header-anchor" href="#利用-yarn-安裝-vue-cli" aria-label="Permalink to “利用 Yarn 安裝 Vue-cli”">&#8203;</a></h1>
<p>這兩天 JavaScript 圈子的最大新聞應該就是 Facebook 發布了一套新的 JavaScript 套件管理工具 Yarn，感覺來勢洶洶勢不可擋。
實際花了一點時間試用，表現的確也比過去 npm 好很多，尤其過去 <code>npm install</code> 速度過慢，套件相依衝突的問題，目前在 Yarn 還沒遇到。 即使是剛開始 <code>yarn run scripts</code> 無法帶入參數的問題，在發布的第二天也快速地更新 (v0.15.1) 解決了。 👍</p>
<img src="/images/notes/yarn-logo.png">
<p>有關 Yarn 介紹的部分，已經有其他前輩分享了，推薦可以看看這篇 <a href="https://medium.com/@jackypan1989/%E5%8F%96%E4%BB%A3-npm-%E7%9A%84%E6%96%B0%E5%88%A9%E5%99%A8-yarn-7d97f2f409b9#.pu0jrdcc4" target="_blank" rel="noreferrer">取代 npm 的新利器 Yarn</a>。</p>
<p>既然 Yarn 號稱可以用來取代 npm，那麼身為 Vue 的愛好者，也想馬上來試試 <a href="https://github.com/vuejs/vue-cli" target="_blank" rel="noreferrer">Vue-cli</a> 這套 Vue 的樣板工具包是否也可以透過 yarn 來單獨執行。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="利用-yarn-安裝-vue-cli" tabindex="-1">利用 Yarn 安裝 Vue-cli <a class="header-anchor" href="#利用-yarn-安裝-vue-cli" aria-label="Permalink to “利用 Yarn 安裝 Vue-cli”">&#8203;</a></h1>
<p>這兩天 JavaScript 圈子的最大新聞應該就是 Facebook 發布了一套新的 JavaScript 套件管理工具 Yarn，感覺來勢洶洶勢不可擋。
實際花了一點時間試用，表現的確也比過去 npm 好很多，尤其過去 <code>npm install</code> 速度過慢，套件相依衝突的問題，目前在 Yarn 還沒遇到。 即使是剛開始 <code>yarn run scripts</code> 無法帶入參數的問題，在發布的第二天也快速地更新 (v0.15.1) 解決了。 👍</p>
<img src="/images/notes/yarn-logo.png">
<p>有關 Yarn 介紹的部分，已經有其他前輩分享了，推薦可以看看這篇 <a href="https://medium.com/@jackypan1989/%E5%8F%96%E4%BB%A3-npm-%E7%9A%84%E6%96%B0%E5%88%A9%E5%99%A8-yarn-7d97f2f409b9#.pu0jrdcc4" target="_blank" rel="noreferrer">取代 npm 的新利器 Yarn</a>。</p>
<p>既然 Yarn 號稱可以用來取代 npm，那麼身為 Vue 的愛好者，也想馬上來試試 <a href="https://github.com/vuejs/vue-cli" target="_blank" rel="noreferrer">Vue-cli</a> 這套 Vue 的樣板工具包是否也可以透過 yarn 來單獨執行。</p>
<hr>
<p><del>既然要取代 npm，那麼第一件事情就是把 npm 整包幹掉</del>，關於刪掉 npm modules 的過程我是參考 stackoverflow 的這篇:
<a href="http://stackoverflow.com/questions/9283472/command-to-remove-all-npm-modules-globally" target="_blank" rel="noreferrer">Command to remove all npm modules globally?</a>，但實際上 Yarn 與 npm 是可以和平共存的，不一定要把 npm 幹掉才能跑 Yarn ，這裡只是想簡單做個試驗，啾咪 ^.&lt;</p>
<p>然後安裝 Yarn。 執行 <code>curl -o- -L https://yarnpkg.com/install.sh | bash</code> 即可。</p>
<hr>
<h3 id="第一步-安裝-vue-cli" tabindex="-1">第一步：安裝 vue-cli <a class="header-anchor" href="#第一步-安裝-vue-cli" aria-label="Permalink to “第一步：安裝 vue-cli”">&#8203;</a></h3>
<p>很簡單，把原本的  <code>npm install -g vue-cli</code> 改成 <code>yarn global add vue-cli</code> 就可以了。執行的時候像這樣：
<img src="/images/notes/yarn-vue-1.png"></p>
<p>而安裝完畢後，執行 <code>yarn vue</code> 應該可以看到這樣的畫面。
<img src="/images/notes/yarn-vue-2.png"></p>
<p>每執行完一個指令還會告訴你它跑了多久喔<del>，非常囂張 (誤)</del>。</p>
<h3 id="第二步-初始化-vue-專案" tabindex="-1">第二步：初始化 Vue 專案 <a class="header-anchor" href="#第二步-初始化-vue-專案" aria-label="Permalink to “第二步：初始化 Vue 專案”">&#8203;</a></h3>
<p>接著，就來初始化我們的 Vue 專案。 Vue-cli 這個 scaffolding 工具官方目前提供了幾種 template 讓開發者自行選擇，
像是大家常見的 webpack、browserify 都有。當然也可以挑選什麼都沒有的 simple: 就是最基本的 Vue 專案這樣。
如果想更詳細了解各種 template 細節的話，可以到 vue-templates 的 <a href="https://github.com/vuejs-templates" target="_blank" rel="noreferrer">Repo</a> 去看。</p>
<p>這裡我就選擇拿「<a href="https://github.com/vuejs-templates/webpack-simple" target="_blank" rel="noreferrer">webpack-simple</a>」當範例。</p>
<p>執行 <code>yarn vue init webpack-simple my-project</code>
<img src="/images/notes/yarn-vue-3.png"></p>
<p>因為 VueJS 目前已經發佈至 2.0 版了，所以你會看到 Vue-cli 很貼心地提醒你現在安裝的是<span style="color: red;"> 2.0 的版本</span>。
若你想安裝的是 1.x 版本的話，可以改成 <br> <code>yarn vue init webpack-simple#1.0 my-project</code> 即可。</p>
<p>然後，Vue-cli 會幫我們建立一個新目錄 <code>my-project</code>。</p>
<p>透過 <code>cd my-project</code> 切換到新專案目錄後，我們將原本的 <code>npm install</code> 改成直接執行 <code>yarn</code> 即可。
<img src="/images/notes/yarn-vue-4.png"></p>
<h3 id="第三步-啟動並執行-vue-專案" tabindex="-1">第三步：啟動並執行 vue 專案 <a class="header-anchor" href="#第三步-啟動並執行-vue-專案" aria-label="Permalink to “第三步：啟動並執行 vue 專案”">&#8203;</a></h3>
<p>最後，直接執行 <code>yarn run dev</code> 應該會看到像這樣的畫面。
<img src="/images/notes/yarn-vue-5.png"></p>
<p>打開你的瀏覽器連到 <code>http://localhost:8080/</code> 看到 <code>Hello Vue!</code> 的字樣就代表成功執行囉！
<img src="/images/notes/yarn-vue-6.png"></p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
            <category>yarn</category>
        </item>
        <item>
            <title><![CDATA[筆記 透過 Composition Events 增強非拉丁語系輸入法對輸入框的支援]]></title>
            <link>https://kurohsu.dev/notes/筆記-透過-Composition-Events-增強非拉丁語系輸入法對輸入框的支援.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/筆記-透過-Composition-Events-增強非拉丁語系輸入法對輸入框的支援.html</guid>
            <pubDate>Tue, 11 Oct 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="筆記-透過-composition-events-增強非拉丁語系輸入法對輸入框的支援" tabindex="-1">筆記 透過 Composition Events 增強非拉丁語系輸入法對輸入框的支援 <a class="header-anchor" href="#筆記-透過-composition-events-增強非拉丁語系輸入法對輸入框的支援" aria-label="Permalink to “筆記 透過 Composition Events 增強非拉丁語系輸入法對輸入框的支援”">&#8203;</a></h1>
<p>最近在爬 Vue 的原始碼的時候，意外發現兩個沒看過的 event：<code>compositionstart</code> 與 <code>compositionend</code>。 查了一下 MDN 才發現這些叫做「<a href="https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent" title="Composition Events" target="_blank" rel="noreferrer">Composition Events</a>」的 event 是從 DOM Level 3 之後才新增的。</p>
<p>介紹 Composition Events 之前先來談談 DOM API 過去對輸入框偵測變化的幾個方式。</p>
<p>一般來說，常見的表單輸入框如: <code>&lt;input type=&quot;text&quot;&gt;</code> 如果要動態監聽輸入框的文字變化時，
大多會透過監聽 <code>keydown</code>、<code>keypress</code>、<code>keyup</code> 等鍵盤事件來判斷 value 是否變動，但如果是透過複製貼上之類的操作，就無法透過鍵盤事件來判斷。</p>
<p>而即使是 <code>change</code> 事件則是要在使用者改變內容，且<span style="color: red;">焦點離開輸入框的前一刻才會被觸發</span>。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="筆記-透過-composition-events-增強非拉丁語系輸入法對輸入框的支援" tabindex="-1">筆記 透過 Composition Events 增強非拉丁語系輸入法對輸入框的支援 <a class="header-anchor" href="#筆記-透過-composition-events-增強非拉丁語系輸入法對輸入框的支援" aria-label="Permalink to “筆記 透過 Composition Events 增強非拉丁語系輸入法對輸入框的支援”">&#8203;</a></h1>
<p>最近在爬 Vue 的原始碼的時候，意外發現兩個沒看過的 event：<code>compositionstart</code> 與 <code>compositionend</code>。 查了一下 MDN 才發現這些叫做「<a href="https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent" title="Composition Events" target="_blank" rel="noreferrer">Composition Events</a>」的 event 是從 DOM Level 3 之後才新增的。</p>
<p>介紹 Composition Events 之前先來談談 DOM API 過去對輸入框偵測變化的幾個方式。</p>
<p>一般來說，常見的表單輸入框如: <code>&lt;input type=&quot;text&quot;&gt;</code> 如果要動態監聽輸入框的文字變化時，
大多會透過監聽 <code>keydown</code>、<code>keypress</code>、<code>keyup</code> 等鍵盤事件來判斷 value 是否變動，但如果是透過複製貼上之類的操作，就無法透過鍵盤事件來判斷。</p>
<p>而即使是 <code>change</code> 事件則是要在使用者改變內容，且<span style="color: red;">焦點離開輸入框的前一刻才會被觸發</span>。</p>
<hr>
<p>所以後來有了 <code>input</code> 事件， input 事件會在<span style="color: red;">輸入框的內容被改變時即時觸發</span>，確實也解決了過去在 onChange 以及鍵盤相關事件帶來的不少問題。</p>
<p>新的問題來了！</p>
<p>通常像這樣的搜尋框，我們會用類似 autocomplete 的方式給使用者搜尋建議 (以 google 為例)：
<img src="/images/notes/google-autocompleted.png"></p>
<p>如上圖，在輸入中文的時候，通常會需要透過注音之類的輸入法來做拼字，但是在大部分的情況下，自動給「注音符號」或是「拼音文字」搜尋建議是沒有太大意義的。</p>
<p>回到主題。這個時候就需要透過 <strong>Composition Events</strong> 來為輸入框做增強。
透過 Composition Events 我們可以觀察使用者在輸入框內開啟輸入法 (Input Method Editor, IME) 時，組字或選字的狀態。</p>
<p>Composition Events 提供三個事件給開發者監聽：分別是 <code>compositionstart</code> 、 <code>compositionend</code> ，以及 <code>compositionupdate</code>。</p>
<ul>
<li><code>compositionstart</code>: 輸入框內開啟輸入法，且正在拼字時觸發。</li>
<li><code>compositionupdate</code>:輸入框內開啟輸入法，且正在拼字或選字時更改了內容時觸發。</li>
<li><code>compositionend</code>: 輸入框內開啟輸入法，拼字或選字完成，正要送出至輸入框時觸發。</li>
</ul>
<p></p>
執行的時候像這樣：
<img src="/images/notes/composition-demo.png">
<p>可以看到，如果要確認使用者輸入完成並送出文字時，可以透過 <code>compositionend</code> 來做最後確認。<br>
附上 JSBin Demo: <a href="http://jsbin.com/mofepimiqo/1/edit?js,console,output" target="_blank" rel="noreferrer">http://jsbin.com/mofepimiqo/1/edit?js,console,output</a></p>
<p>最後則是大家都很關心的瀏覽器支援度。以目前來說，mobile 平台上可能還不太 ok，但是 Desktop 平台上表現看起來相當不錯，可以大膽地使用囉。
<img src="/images/notes/compositionEvent-compatibility.png"></p>
<p class="no-space" style="margin-bottom:0">參考資料：</p>
* [用 Composition Event 改進 CodeMirror 對輸入法的支援](http://blog.zhusee.in/post/146553/enhance-ime-support-of-codemirror-with-composition-events)
* [MDN: Composition Events](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent "Composition Events")]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>HTML</category>
            <category>html</category>
            <category>javascript</category>
            <category>events</category>
        </item>
        <item>
            <title><![CDATA[VueJS 各種資料綁定 (data binding) 的方式]]></title>
            <link>https://kurohsu.dev/notes/VueJS-各種資料綁定-data-binding-的方式.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/VueJS-各種資料綁定-data-binding-的方式.html</guid>
            <pubDate>Wed, 05 Oct 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="vuejs-各種資料綁定-data-binding-的方式" tabindex="-1">VueJS 各種資料綁定 (data binding) 的方式 <a class="header-anchor" href="#vuejs-各種資料綁定-data-binding-的方式" aria-label="Permalink to “VueJS 各種資料綁定 (data binding) 的方式”">&#8203;</a></h1>
<p>大部分資料是從這裡來的: <a href="https://github.com/vuejs/vue/wiki/1.0.0-binding-syntax-cheatsheet" target="_blank" rel="noreferrer">Vue 1.0.0 binding syntax cheatsheet</a>，再加上對 Vue 2.0 補充的部分。</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- normal directive --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ok"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- directive with argument --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-on:click</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"onClick"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<h1 id="vuejs-各種資料綁定-data-binding-的方式" tabindex="-1">VueJS 各種資料綁定 (data binding) 的方式 <a class="header-anchor" href="#vuejs-各種資料綁定-data-binding-的方式" aria-label="Permalink to “VueJS 各種資料綁定 (data binding) 的方式”">&#8203;</a></h1>
<p>大部分資料是從這裡來的: <a href="https://github.com/vuejs/vue/wiki/1.0.0-binding-syntax-cheatsheet" target="_blank" rel="noreferrer">Vue 1.0.0 binding syntax cheatsheet</a>，再加上對 Vue 2.0 補充的部分。</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- normal directive --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ok"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- directive with argument --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-on:click</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"onClick"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- v-on with argument + key modifier --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-on:keyup.enter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"handleEnter"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- literal modifier: pass literal string to directive for Vue 1.x, --></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- but deprecated in 2.0 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-link.literal</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/a/b/c"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- binding an attribute with v-bind --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">img</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-bind:src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"imgSrc"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-bind:href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"baseURL + '/path'"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- shorthand: colon for v-bind --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">img</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> :src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"imgSrc"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> :href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"baseURL + '/path'"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- shorthand: @ for v-on --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> @click</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"onClick"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- key modifier --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> @keyup.enter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"handleEnter"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- stop/prevent modifier --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> @click.stop</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"onClick"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">form</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> @submit.prevent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- props for Vue 1.x --></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- .once and .sync are deprecated in 2.0 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">my-comp</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  prop</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"literal string"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  v-bind:prop</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"defaultOneWay"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  v-bind:prop.sync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"twoWay"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  v-bind:prop.once</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"oneTime"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">my-comp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- props with one time binding for Vue 2.0 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">my-comp</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  prop</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"literal string"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  v-bind:prop</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"defaultOneWay"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  v-once</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">my-comp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- component with props and custom events, in shorthand --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">item-list</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  :items</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"items"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  :open.sync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"isListOpen"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  @ready</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"onItemsReady"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  @update</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"onItemsUpdate"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">item-list</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- v-el and v-ref now just use the argument --></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- registers vm.$refs.child for Vue 1.x --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">comp</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-ref:child</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">comp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- registers vm.$refs.child for Vue 2.0 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">comp</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ref:child</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">comp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- registers vm.$els.node for Vue 1.x, but deprecated in 2.0 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-el:node</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br><span class="line-number">55</span><br><span class="line-number">56</span><br><span class="line-number">57</span><br><span class="line-number">58</span><br><span class="line-number">59</span><br><span class="line-number">60</span><br><span class="line-number">61</span><br><span class="line-number">62</span><br><span class="line-number">63</span><br><span class="line-number">64</span><br><span class="line-number">65</span><br><span class="line-number">66</span><br><span class="line-number">67</span><br><span class="line-number">68</span><br></div></div>]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
        </item>
        <item>
            <title><![CDATA[VueJS V1 與 V2 元件實體之差異]]></title>
            <link>https://kurohsu.dev/notes/VueJS-V1-與-V2-元件實體之差異.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/VueJS-V1-與-V2-元件實體之差異.html</guid>
            <pubDate>Mon, 03 Oct 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="vuejs-v1-與-v2-元件實體之差異" tabindex="-1">VueJS V1 與 V2 元件實體之差異 <a class="header-anchor" href="#vuejs-v1-與-v2-元件實體之差異" aria-label="Permalink to “VueJS V1 與 V2 元件實體之差異”">&#8203;</a></h1>
<p>狂賀！ <a href="https://github.com/vuejs/vue/releases/tag/v2.0.0" title="Vue 2.0" target="_blank" rel="noreferrer">Vue 2.0</a> 終於正式發佈！</p>
<p>關於 Vue 2.0 的新特性，作者也在官方 Blog - <a href="https://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.n0m3yjfmp" title="Vue 2.0 is Here!" target="_blank" rel="noreferrer">Vue 2.0 is Here!</a> (<a href="http://jiongks.name/blog/vue-2-is-here/" title="Vue 2.0 来了！" target="_blank" rel="noreferrer">中文翻譯</a>) 一文中敘述地相當詳細，這裡就不多說。</p>
<p>如果你也與我一樣是從 V1 就開始接觸的開發者，一定都知道 VueJS 最核心的一部分是 Component，而 Component 是由實體 (Vue Instance) 來實作。
在這篇文章，我們就來談談 Vue 1.x 與 2.x 元件實體的差異。</p>
<hr>
]]></description>
            <content:encoded><![CDATA[<h1 id="vuejs-v1-與-v2-元件實體之差異" tabindex="-1">VueJS V1 與 V2 元件實體之差異 <a class="header-anchor" href="#vuejs-v1-與-v2-元件實體之差異" aria-label="Permalink to “VueJS V1 與 V2 元件實體之差異”">&#8203;</a></h1>
<p>狂賀！ <a href="https://github.com/vuejs/vue/releases/tag/v2.0.0" title="Vue 2.0" target="_blank" rel="noreferrer">Vue 2.0</a> 終於正式發佈！</p>
<p>關於 Vue 2.0 的新特性，作者也在官方 Blog - <a href="https://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.n0m3yjfmp" title="Vue 2.0 is Here!" target="_blank" rel="noreferrer">Vue 2.0 is Here!</a> (<a href="http://jiongks.name/blog/vue-2-is-here/" title="Vue 2.0 来了！" target="_blank" rel="noreferrer">中文翻譯</a>) 一文中敘述地相當詳細，這裡就不多說。</p>
<p>如果你也與我一樣是從 V1 就開始接觸的開發者，一定都知道 VueJS 最核心的一部分是 Component，而 Component 是由實體 (Vue Instance) 來實作。
在這篇文章，我們就來談談 Vue 1.x 與 2.x 元件實體的差異。</p>
<hr>
<hr>
<h3 id="vue-2-0-元件實體註冊" tabindex="-1">Vue 2.0 元件實體註冊 <a class="header-anchor" href="#vue-2-0-元件實體註冊" aria-label="Permalink to “Vue 2.0 元件實體註冊”">&#8203;</a></h3>
<img alt="tree of components" src="/images/notes/vue-instance.png">
<p>像上面這樣的網站，我們可以將它抽象化為一棵「元件樹」，而每個元件樹都會有個根節點，或稱為根實體 (root Vue instance)。</p>
<p>那麼，每個 Vue 元件樹的根實體其實是透過 <code>Vue</code> 這個建構函式所產生：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> vm </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // options</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>將 Vue 元件與實體 DOM 結合的方式有兩種，一種是直接寫在 <code>el</code> option 內：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> vm </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  el: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#app'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>而另一種方式則是透過 <code>$mount</code> 來指定節點：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> vm </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // options without 'el'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">vm.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$mount</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#app'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>這部分跟 Vue 1.x 的註冊是完全一樣的，但是需要注意的是，在 vue 1.x 允許開發者以 <code>&lt;body&gt;</code> 或 <code>&lt;html&gt;</code> 作為根實體的掛載點，
但<span style="color: #f00">到了 VueJS 2.0 後，只能透過 <strong>獨立的節點掛載</strong> </span>，如: div 等。 否則會產生錯誤，警告訊息如下：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>“Do not mount Vue to &#x3C;html> or &#x3C;body> - mount to normal elements instead.“</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>換成用獨立的 DOM 節點，如 <code>&lt;div id=&quot;app&quot;&gt;&lt;/div&gt;</code>，就可以正常運作了。</p>
<hr>
<h3 id="vue-2-0-元件實體的生命週期" tabindex="-1">Vue 2.0 元件實體的生命週期 <a class="header-anchor" href="#vue-2-0-元件實體的生命週期" aria-label="Permalink to “Vue 2.0 元件實體的生命週期”">&#8203;</a></h3>
<img alt="Vue 2.0 Lifecycle Diagram" title="Vue 2.0 Lifecycle Diagram" src="/images/notes/vue2-lifecycle.png" height="400">
<span style="font-size: 12px;">圖片來源： [Vue 2.0 Guide: Instance Lifecycle Hooks](http://vuejs.org/guide/instance.html#Instance-Lifecycle-Hooks)</span>
<p>基本上 Vue 2.0 實體生命週期中，大部分的階段都與 Vue 1.x 是一樣的，最大的不同在於 lifecycle hook 名稱的改變，以及在元件被掛載 <code>mounted</code> 之後，還新增了 <code>beforeUpdate</code> 以及 <code>updated</code> 這兩組偵測更新的 hook。</p>
<p>vue 1.x 的 <code>init</code> 變成 <code>beforeCreate</code> ， <code>beforeCompiled</code> 改為 <code>beforeMount</code>。 而原本的 <code>complied</code> 與 <code>ready</code> 則是統一收斂成 <code>mounted</code>。</p>
<p>另外需要注意的是，<span style="color: #f00"><strong>若元件本身是透過 server-side rendering 的話，除了 <code>beforeCreate</code> 以及 <code>created</code> 以外的所有 hook 都不會被呼叫</strong></span>  <a href="https://vuejs.org/api/#Options-Lifecycle-Hooks" target="_blank" rel="noreferrer">(參考資料)</a>。</p>
<p>有關元件 V-DOM 的重新渲染與更新後面再提，其他部分則與 Vue 1.x 大同小異。</p>
<hr>
### Vue 2.0 元件與模板的編譯 - Render Function
<p>在大部分情況下，透過元件的 <code>template</code> 屬性，或是直接寫在 HTML 中就已經足夠操作你的元件了。
不過若是你想完全透過 JavaScript 來操作你的元件，那麼可以使用 render 這個 function 直接來寫底層的 virtual-DOM 來取代 <code>template</code> 屬性。
VueJS 2.0 的 virtual DOM 機制，是採用 <a href="https://github.com/snabbdom/snabbdom" title="snabbdom" target="_blank" rel="noreferrer">snabbdom</a> 這個 virtual DOM 的 library 來實作的。</p>
<p>可以使用 <code>createElement</code> 這個 function 來建立你的元件內容：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// @returns {VNode}</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // {String | Object | Function}</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // An HTML tag name, component options, or function</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // returning one of these. Required.</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'div'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // {Object}</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // A data object corresponding to the attributes</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // you would use in a template. Optional.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // (see details in the next section below)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // {String | Array}</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // Children VNodes. Optional.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  [</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    createElement</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'h1'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'hello world'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    createElement</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(MyComponent, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      props: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        someProp: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'foo'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }),</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    'bar'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br></div></div><p>官方也提供了一個完整的 render function 範例：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getChildrenTextContent</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">children</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> children.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">node</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> node.children</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      ?</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getChildrenTextContent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(node.children)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      :</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> node.text</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">Vue.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">component</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'anchored-heading'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  render</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">createElement</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // create kebabCase id</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> headingId </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getChildrenTextContent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.$slots.default)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toLowerCase</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\W</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'-'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">^</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\-</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">|</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\-</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">$</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">)</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">g</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> createElement</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      'h'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.level,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      [</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">        createElement</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'a'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          attrs: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            name: headingId,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            href: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> headingId</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.$slots.default)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    )</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  props: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    level: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      type: Number,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      required: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br></div></div><p>當然，你可能跟我一樣覺得一層又一層的 <code>createElement</code> 看了總是讓人厭煩，你也可以透過這個 Plugin: <a href="https://github.com/vuejs/babel-plugin-transform-vue-jsx" target="_blank" rel="noreferrer">babel-plugin-transform-vue-jsx</a>，來做 JSX 語法的轉換，如果你曾是 react 應用程式的開發者，應該對 JSX 語法不陌生。 寫起來會像這樣：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> AnchoredHeading </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> './AnchoredHeading.vue'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Vue</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  el: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#demo'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  render</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">h</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">AnchoredHeading</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> level</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">span</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>Hello&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">span</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> world!</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">AnchoredHeading</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    )</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><p>在預設情況中，VueJS 2.0 會將 <code>template</code> 內的 HTML 透過 parse 轉換成 AST，再自動轉換優化成 render function 去建立 virtual DOM。 在建立 virtual DOM 之後，透過 observe 機制與資料進行綁定，再 compile 成實體的 DOM 並渲染至網頁上：</p>
<img src="/images/notes/vue2-rendering-flow.png">
<span style="font-size: 12px;">參考資料與圖片來源： [Next Vue.js 2.0 by kazupon](https://speakerdeck.com/kazupon/next-vue-dot-js-2-dot-0)</span>
<p>前面說過，VueJS 2.0 會將 <code>template</code> 內的 HTML 自動編譯成 render function，下面這是官方文件以 <code>Vue.compile</code> 提供的 demo：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">&#x3C;!-- template --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>I'm a template!&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"message"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    {{ message }}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    No message.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// render:</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> anonymous</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  with</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> _h</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'div'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">_m</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),(message)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">_h</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'p'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">_s</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(message)])</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">_h</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'p'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,[</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"No message."</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">])])}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// staticRenderFns:</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">_m</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">): </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> anonymous</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  with</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> _h</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'h1'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,[</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"I'm a template!"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">])}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><hr>
### Vue 2.0 元件的追蹤變化
<p>最後，我們來看看元件內狀態的追蹤變化。有寫過 VueJS 1.x 的朋友應該知道，元件實體內有個 option 叫 <code>data</code>，
這個 <code>data</code> 物件就是用來存放元件內狀態/資料的地方。</p>
<p>與 Vue 1.x 相同的地方是，<code>data</code> 物件透過 <code>Object.defineProperty()</code> 來為元件內各屬性設定 「getter」與「setter」。
就在 data 屬性被存取修改時，會透過 getter/setter 來通知物件內屬性的變化，當先前設定好的 setter 被呼叫的時候，會去觸發 watcher 重新計算，也就會導致 DOM 的更新與重新渲染。</p>
<p>與 Vue 1.x 不同的是，Vue 1.x 是透過 directive 來重新渲染 DOM 內容：</p>
<img src="/images/notes/vue1-watcher.png">
<p>而 Vue 2.0 在通知 watcher 更新時，會去呼叫前面介紹的 「render function」與更新後的 data 去做更新後再次渲染，概念與 1.x 大致相同。
但更新 DOM 的手法不同，減少了不必要的比對，也因此大幅度提升了效能。</p>
<img src="/images/notes/vue2-watcher.png">
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
        </item>
        <item>
            <title><![CDATA[VueJS 2.0 升級小幫手: Vue migration helper]]></title>
            <link>https://kurohsu.dev/notes/Vue-2-0-升級小幫手-Vue-migration-helper.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/Vue-2-0-升級小幫手-Vue-migration-helper.html</guid>
            <pubDate>Fri, 30 Sep 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="vuejs-2-0-升級小幫手-vue-migration-helper" tabindex="-1">VueJS 2.0 升級小幫手: Vue migration helper <a class="header-anchor" href="#vuejs-2-0-升級小幫手-vue-migration-helper" aria-label="Permalink to “VueJS 2.0 升級小幫手: Vue migration helper”">&#8203;</a></h1>
<p>Vue JS 自今年四月 (2016/04) 發佈 Vue 2.0 preview 版本至今也有五個月了，在新舊版本的交替之中，開發者最關心的一定是「<strong>我的專案能不能升到 Vue 2.0</strong>」、「<strong>升上去會不會爆</strong>」、「<strong>專案該用那個版本來開發</strong>」，<del>「聽說隔壁那個 ng 升級幾乎等於砍掉重練」</del> 之類的問題。</p>
<p>不過幸好，Vue 1.0 與 Vue 2.0 有 90% 的 API 是相同的，過去在 Vue 1.x 的核心概念到 Vue 2.0 一樣可以沿用，而且又多了些新特性。</p>
<p>這裡有一份官方的<a href="http://rc.vuejs.org/guide/migration.html" title="Migration from Vue 1.x" target="_blank" rel="noreferrer">升級建議</a>，有點長，如果沒有耐心讀的話，沒關係，這裡介紹你好物: <br> <strong><span style="color: #f00;">Vue migration helper</span></strong>。</p>
<p>傳說中的升級小幫手 Vue migration helper: <a href="https://github.com/vuejs/vue-migration-helper" target="_blank" rel="noreferrer">https://github.com/vuejs/vue-migration-helper</a></p>
]]></description>
            <content:encoded><![CDATA[<h1 id="vuejs-2-0-升級小幫手-vue-migration-helper" tabindex="-1">VueJS 2.0 升級小幫手: Vue migration helper <a class="header-anchor" href="#vuejs-2-0-升級小幫手-vue-migration-helper" aria-label="Permalink to “VueJS 2.0 升級小幫手: Vue migration helper”">&#8203;</a></h1>
<p>Vue JS 自今年四月 (2016/04) 發佈 Vue 2.0 preview 版本至今也有五個月了，在新舊版本的交替之中，開發者最關心的一定是「<strong>我的專案能不能升到 Vue 2.0</strong>」、「<strong>升上去會不會爆</strong>」、「<strong>專案該用那個版本來開發</strong>」，<del>「聽說隔壁那個 ng 升級幾乎等於砍掉重練」</del> 之類的問題。</p>
<p>不過幸好，Vue 1.0 與 Vue 2.0 有 90% 的 API 是相同的，過去在 Vue 1.x 的核心概念到 Vue 2.0 一樣可以沿用，而且又多了些新特性。</p>
<p>這裡有一份官方的<a href="http://rc.vuejs.org/guide/migration.html" title="Migration from Vue 1.x" target="_blank" rel="noreferrer">升級建議</a>，有點長，如果沒有耐心讀的話，沒關係，這裡介紹你好物: <br> <strong><span style="color: #f00;">Vue migration helper</span></strong>。</p>
<p>傳說中的升級小幫手 Vue migration helper: <a href="https://github.com/vuejs/vue-migration-helper" target="_blank" rel="noreferrer">https://github.com/vuejs/vue-migration-helper</a></p>
<hr>
<p>使用方式很簡單，打開 terminal 透過 npm 安裝後，在你的專案目錄下執行 vue-migration-helper，小幫手就會幫你掃描整份專案，然後給你修改的建議了。</p>
<p>完整的安裝與使用方式：</p>
<div class="language- line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span># install</span></span>
<span class="line"><span>$ npm install --global git://github.com/vuejs/vue-migration-helper.git</span></span>
<span class="line"><span></span></span>
<span class="line"><span># navigate to a Vue 1.x project directory</span></span>
<span class="line"><span>$ cd path/to/my-vue-project</span></span>
<span class="line"><span></span></span>
<span class="line"><span># scan all files in the current directory</span></span>
<span class="line"><span>$ vue-migration-helper</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>拿之前的某份範例試試，就像這樣：</p>
<p><del>花惹發</del>，滿坑滿谷的升級建議
<img src="/images/notes/vue-migration-demo.png" alt="migration"></p>
<p>依照 migration helper 給的建議一一修正後，再次執行專案，檢查一下 console，是否有噴錯，如果沒有錯誤，那麼專案的升級也就差不多了。</p>
<p>不過要注意，目前 Vue migration helper 還在 beta 階段，如果未來要再更新至新版的話，直接再次執行 <code>npm install --global git://github.com/vuejs/vue-migration-helper.git</code> 重新安裝一次就好。</p>
<p>最後，鄉親啊，如果你正要開啟新專案，建議現在就可以直接從 Vue 2.0 進入喔！</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
        </item>
        <item>
            <title><![CDATA[VueJS 在 v-for 列表中透過 filter 完成搜尋與分頁的功能]]></title>
            <link>https://kurohsu.dev/notes/vuejs-in-v-for-through-the-filter-in-the-list-complete-search-and-page-functions.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/vuejs-in-v-for-through-the-filter-in-the-list-complete-search-and-page-functions.html</guid>
            <pubDate>Mon, 30 May 2016 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>最近 <a href="https://vuejs.org/" target="_blank" rel="noreferrer">Vue.js</a> 正夯，所以手上幾個東西打算用這個來改寫，關於 Vue.js 的基本介紹可以參考小弟的投影片，這裡就不再贅述。</p>
<iframe src="//www.slideshare.net/slideshow/embed_code/key/DLQDvLrRSNunsY" width="476" height="400" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:10px; max-width: 100%;" allowfullscreen> </iframe>
<p>有用過 Vue.js 開發的朋友一定知道它提供的 filter 功能十分強大，在 <code>v-for</code> 列表中使用 <code>filterBy</code> 可以在一行內完成列表搜尋的功能：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"filter-by-example"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-model</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"n"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">ul</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<p>最近 <a href="https://vuejs.org/" target="_blank" rel="noreferrer">Vue.js</a> 正夯，所以手上幾個東西打算用這個來改寫，關於 Vue.js 的基本介紹可以參考小弟的投影片，這裡就不再贅述。</p>
<iframe src="//www.slideshare.net/slideshow/embed_code/key/DLQDvLrRSNunsY" width="476" height="400" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:10px; max-width: 100%;" allowfullscreen> </iframe>
<p>有用過 Vue.js 開發的朋友一定知道它提供的 filter 功能十分強大，在 <code>v-for</code> 列表中使用 <code>filterBy</code> 可以在一行內完成列表搜尋的功能：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"filter-by-example"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-model</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"n"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">ul</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      &#x3C;!-- 透過 input 欄位的 v-model n 與 user.name 做模糊比對 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"user in users | filterBy n in 'name'"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        {{ user.name }}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">li</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">ul</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br></div></div><p>若是要限制顯示的筆數也能用 <code>limitBy</code> 做到，進而完成分頁的功能。</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  &#x3C;!-- 只顯示前 10 個元素 --></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"item in items | limitBy 10"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  &#x3C;!-- 顯示第 5 到 15 筆元素--></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"item in items | limitBy 10 5"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>這次遇到的問題是這樣的，如果我們想要同時完成「搜尋」與「分頁」的需求，光靠 <code>filterBy</code> 與 <code>limitBy</code> 就不是那麼容易做到，還好 Vue.js 提供了<a href="http://vuejs.org/guide/custom-filter.html" target="_blank" rel="noreferrer">自訂 filter</a> 的功能：先用 filterBy 過濾，再透過自訂 recordLength 記錄過濾後的資料數量，最後再用 limitBy 搭配頁籤切換頁面。
小心 filter 的順序，filter 會依序執行，然後再繼續下個 filter。</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">" r in rows</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">               | filterBy filter_name in 'name'</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">               | recordLength 'filteredRowCount'</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">               | limitBy countOfPage pageStart "</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>......&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">td</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">tr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>然後是自定的 filter <code>recordLength</code>：
result 代表傳入的資料， key 則是從 view 帶入的參數，這個範例是 <code>filteredRowCount</code>。</p>
<p>這裡透過 <code>vm.$set</code> 來將過濾後的數量指定至 vue 實體，以便可以直接在 Vue 實體使用。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  Vue.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'recordLength'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">key</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$set</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(key, result.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> result;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p>最後在頁籤的部分，我們就可以簡單透過 <code>filter_name</code> 欄位是否空白來切換是否透過 <code>filteredRowCount</code> 計算總頁數:</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  totalPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.filter_name.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ceil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.rows.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> /</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.countOfPage);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ceil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.filteredRowCount </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.countOfPage);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>完整的 <a href="http://jsbin.com/bimawidora/1/edit?html,js,output" target="_blank" rel="noreferrer">demo</a> 如下:</p>
<p><a class="jsbin-embed" href="http://jsbin.com/bimawidora/embed?output">JS Bin on jsbin.com</a><script src="http://static.jsbin.com/js/embed.min.js?3.35.12"></script></p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>vue.js</category>
        </item>
        <item>
            <title><![CDATA[利用 d3-js 製作 responsive 的長條圖]]></title>
            <link>https://kurohsu.dev/notes/use-d3js-to-create-responsive-histogram.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/use-d3js-to-create-responsive-histogram.html</guid>
            <pubDate>Sat, 19 Dec 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="利用-d3-js-製作-responsive-的長條圖" tabindex="-1">利用 d3-js 製作 responsive 的長條圖 <a class="header-anchor" href="#利用-d3-js-製作-responsive-的長條圖" aria-label="Permalink to “利用 d3-js 製作 responsive 的長條圖”">&#8203;</a></h1>
<p>利用 d3-js 我們可以很輕易地產生我們想要的圖表，以最常見的長條圖為例，只要透過 scale (比例尺) 與 axis (軸線)，再加上一點 SVG 的基礎知識，像這樣的長條圖一下子就可以生成。</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/2V8wHgLUTEawU5iJstYS_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-12-20%20%E4%B8%8B%E5%8D%8812.21.25.png" alt="螢幕快照 2015-12-20 下午12.21.25.png">
[<a href="http://kurotanshi.github.io/d3js-samples/rwd/rwd.html" title="長條圖範例" target="_blank" rel="noreferrer">Code</a>]</p>
<p>但是，像這樣尺寸的圖表，往往都會因為太大而不適合在手機螢幕上呈現。還好 SVG 有著向量圖形的特性，可以自由縮放，這篇就來簡單介紹 d3-js 的長條圖如何也能做出 rwd 的效果。</p>
<p>在上面的程式碼內，我們可以看到，原先設定的寬高是寫死的 960 與 500 (未扣除邊界)</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="利用-d3-js-製作-responsive-的長條圖" tabindex="-1">利用 d3-js 製作 responsive 的長條圖 <a class="header-anchor" href="#利用-d3-js-製作-responsive-的長條圖" aria-label="Permalink to “利用 d3-js 製作 responsive 的長條圖”">&#8203;</a></h1>
<p>利用 d3-js 我們可以很輕易地產生我們想要的圖表，以最常見的長條圖為例，只要透過 scale (比例尺) 與 axis (軸線)，再加上一點 SVG 的基礎知識，像這樣的長條圖一下子就可以生成。</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/2V8wHgLUTEawU5iJstYS_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-12-20%20%E4%B8%8B%E5%8D%8812.21.25.png" alt="螢幕快照 2015-12-20 下午12.21.25.png">
[<a href="http://kurotanshi.github.io/d3js-samples/rwd/rwd.html" title="長條圖範例" target="_blank" rel="noreferrer">Code</a>]</p>
<p>但是，像這樣尺寸的圖表，往往都會因為太大而不適合在手機螢幕上呈現。還好 SVG 有著向量圖形的特性，可以自由縮放，這篇就來簡單介紹 d3-js 的長條圖如何也能做出 rwd 的效果。</p>
<p>在上面的程式碼內，我們可以看到，原先設定的寬高是寫死的 960 與 500 (未扣除邊界)</p>
<hr>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> margin </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 40</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      width </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 960</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> margin</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      height </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 500</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> margin</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>所以第一步，我們先將外層的 <code>.content</code> 元素設定成寬高 100%，然後把原先寫死的寬高改成由程式去抓取實際的寬高</p>
<div class="language-css line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  display</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">block</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  width</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">100</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">%</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  height</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">100</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">%</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  min-width</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">300</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">px</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  max-width</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">960</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">px</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  max-height</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">500</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">px</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  overflow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">hidden</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"> // 將尺寸改成即時取得的寬高</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> width </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> parseInt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">".content"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"width"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> margin</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> height </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> parseInt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">".content"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"height"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> margin</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>然後我們將瀏覽器縮小之後重整，結果像這樣：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/Jrbqo6EaTJswhfBQUF6g_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-12-20%20%E4%B8%8B%E5%8D%881.23.36.png" alt="螢幕快照 2015-12-20 下午1.23.36.png">
[<a href="http://kurotanshi.github.io/d3js-samples/rwd/rwd2.html" title="長條圖範例2" target="_blank" rel="noreferrer">Code</a>]</p>
<p>可以看到，這個時候因為畫面的寬高已經不是寫死的了，所以會依「繪製圖形當下」的寬高去做比例的修正。這時我們已經完成了長條圖 RWD 的第一步了。</p>
<p>這時要是將行動裝置螢幕橫擺後，比例並不會依照橫擺之後有所不同，如果要使用者不斷地重整頁面，這就太不友善了。所以，我們要在瀏覽器上加上 <code>resize</code> 事件，並將繪製圖形的動作通通封裝至 rendering 這個 function 內。</p>
<p>這個時候的程式架構會像這樣：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> svg </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.svg'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 將繪製動作包裝至 function 內</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> rendering</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">     // 將繪製的程式碼通通搬到裡面</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">     // 內略</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 將 window 綁定 resize 事件，並重新繪製圖型</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(window).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">on</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'resize'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, rendering);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 首次繪製</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  rendering</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br></div></div><p>所以，這個時候，我們可以任意拉放瀏覽器的尺寸
像這樣
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/eTI8r7EXSQWaH80HZuKH_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-12-20%20%E4%B8%8B%E5%8D%881.39.54.png" alt="螢幕快照 2015-12-20 下午1.39.54.png"></p>
<p>或是這樣
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/NMpkG3LeTOWaYaNf9pYC_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-12-20%20%E4%B8%8B%E5%8D%881.40.01.png" alt="螢幕快照 2015-12-20 下午1.40.01.png"></p>
<p>[<a href="http://kurotanshi.github.io/d3js-samples/rwd/rwd3.html" title="長條圖範例3" target="_blank" rel="noreferrer">Code</a>]
都是沒有問題的。</p>
<p>這份長條圖到目前為止已經可以說是好棒棒了。
可是不曉得有沒有人發現，在最後那張橫擺的 y 軸刻度實在太過擁擠，其實是不容易閱讀的:
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/VA6ir3TRyutauy6ALqdH_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-12-20%20%E4%B8%8B%E5%8D%881.42.43.png" alt="螢幕快照 2015-12-20 下午1.42.43.png"></p>
<p>我們可以怎麼樣更優化呢？</p>
<p>這時候就要利用 d3-js 提供的 <code>tick()</code> 功能，來為我們調整 y 軸上的刻度。
只要在 y 軸上加上 tick ，像這樣:</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // y 軸加上 ticks</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> yAxis </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.svg.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">axis</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">scale</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(yScale2)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">orient</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"left"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ticks</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">max</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(height</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">50</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p><code>Math.max</code> 會回傳兩個指定數字中較大的一個，而 <code>ticks()</code> 則是設定軸線上刻度的數量。</p>
<p>所以經過剛剛的設定，當圖形的高度大於 100px 的時候，圖表每 50px 會有一個刻度，而圖形高度小於或等於 100px 時，則至少會有兩個刻度，像這樣：</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/Mz3vq9BKRZ2dkSd5SP6W_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-12-20%20%E4%B8%8B%E5%8D%881.54.29.png" alt="螢幕快照 2015-12-20 下午1.54.29.png"></p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/swZcbhnQPmhiWQBwTbDG_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-12-20%20%E4%B8%8B%E5%8D%881.55.08.png" alt="螢幕快照 2015-12-20 下午1.55.08.png"></p>
<p>[<a href="http://kurotanshi.github.io/d3js-samples/rwd/rwd4.html" title="長條圖範例4" target="_blank" rel="noreferrer">Code</a>]</p>
<p>實際用手機試試：</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/mM3agzeTGGGdM2p4YjZq_2015-12-20%2003.33.30.png" alt="2015-12-20 03.33.30.png">
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/378297/j4wccnAOTZWhLzubTUDw_2015-12-20%2003.33.43.png" alt="2015-12-20 03.33.43.png"></p>
<p>透過這樣的修正，就可以讓圖表變得更好閱讀了。</p>
<p>以上，我們只要做簡單的小調整，就可以讓現有的 d3 長條圖做到有 RWD 的效果。</p>
<p>但是要注意的是，不是所有圖表都合適在手機螢幕上呈現，在設計時也需要把這些考慮進去，是要為手機版本另外做一個新的圖表，或是做 RWD 的設計，就看使用的情境以及想表達的意義來決定囉。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>D3-js</category>
            <category>d3-js</category>
            <category>rwd</category>
        </item>
        <item>
            <title><![CDATA[利用 Google Fusion Table，不用寫 code 也可以產生主題地圖]]></title>
            <link>https://kurohsu.dev/notes/using-google-fusion-table-without-writing-code-or-topic-maps.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/using-google-fusion-table-without-writing-code-or-topic-maps.html</guid>
            <pubDate>Wed, 14 Oct 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="利用-google-fusion-table-不用寫-code-也可以產生主題地圖" tabindex="-1">利用 Google Fusion Table，不用寫 code 也可以產生主題地圖 <a class="header-anchor" href="#利用-google-fusion-table-不用寫-code-也可以產生主題地圖" aria-label="Permalink to “利用 Google Fusion Table，不用寫 code 也可以產生主題地圖”">&#8203;</a></h1>
<p>感謝台北市政府以及<a href="https://www.facebook.com/photo.php?fbid=10204828212414899&amp;set=a.1357755313713.2044236.1526408898&amp;type=3" target="_blank" rel="noreferrer">相關人士的努力</a>，在十月中旬的時候，台北市 open data 平台又開放了「<a href="http://data.taipei/opendata/datalist/datasetMeta?oid=68785231-d6c5-47a1-b001-77eec70bec02" target="_blank" rel="noreferrer">台北市住宅竊盜點位資訊</a>」這樣的資料，雖然很多人戲稱房價又要下跌了，但是老話一句，身為<del>有良心的開發者，當然居住安全比房價什麼的還要重要得多</del>。</p>
<p>那麼，拿到這份資料我們可以怎麼玩呢?
剛好這份資料的格式是 CSV (Comma-Separated Values，一種由逗點分隔的純文字資料格式)，所以本篇就來介紹如何透過 <a href="https://support.google.com/fusiontables/answer/2571232" target="_blank" rel="noreferrer">Google Fusion Table</a> 來讓我們不必寫任何的 code ，也可以建立主題地圖。</p>
<p>首先先到<a href="http://data.taipei/opendata/datalist/datasetMeta?oid=68785231-d6c5-47a1-b001-77eec70bec02" target="_blank" rel="noreferrer">台北市住宅竊盜點位資訊</a>，點一下「使用資料」將所需的檔案下載下來，會得到一份 csv 檔案。</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/UsvTpRmSDWhtT8junGNw_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.16.22.png" alt="螢幕快照 2015-10-15 上午10.16.22.png"></p>
]]></description>
            <content:encoded><![CDATA[<h1 id="利用-google-fusion-table-不用寫-code-也可以產生主題地圖" tabindex="-1">利用 Google Fusion Table，不用寫 code 也可以產生主題地圖 <a class="header-anchor" href="#利用-google-fusion-table-不用寫-code-也可以產生主題地圖" aria-label="Permalink to “利用 Google Fusion Table，不用寫 code 也可以產生主題地圖”">&#8203;</a></h1>
<p>感謝台北市政府以及<a href="https://www.facebook.com/photo.php?fbid=10204828212414899&amp;set=a.1357755313713.2044236.1526408898&amp;type=3" target="_blank" rel="noreferrer">相關人士的努力</a>，在十月中旬的時候，台北市 open data 平台又開放了「<a href="http://data.taipei/opendata/datalist/datasetMeta?oid=68785231-d6c5-47a1-b001-77eec70bec02" target="_blank" rel="noreferrer">台北市住宅竊盜點位資訊</a>」這樣的資料，雖然很多人戲稱房價又要下跌了，但是老話一句，身為<del>有良心的開發者，當然居住安全比房價什麼的還要重要得多</del>。</p>
<p>那麼，拿到這份資料我們可以怎麼玩呢?
剛好這份資料的格式是 CSV (Comma-Separated Values，一種由逗點分隔的純文字資料格式)，所以本篇就來介紹如何透過 <a href="https://support.google.com/fusiontables/answer/2571232" target="_blank" rel="noreferrer">Google Fusion Table</a> 來讓我們不必寫任何的 code ，也可以建立主題地圖。</p>
<p>首先先到<a href="http://data.taipei/opendata/datalist/datasetMeta?oid=68785231-d6c5-47a1-b001-77eec70bec02" target="_blank" rel="noreferrer">台北市住宅竊盜點位資訊</a>，點一下「使用資料」將所需的檔案下載下來，會得到一份 csv 檔案。</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/UsvTpRmSDWhtT8junGNw_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.16.22.png" alt="螢幕快照 2015-10-15 上午10.16.22.png"></p>
<hr>
<p>接著打開 <a href="https://support.google.com/fusiontables/answer/2571232" target="_blank" rel="noreferrer">Google Fusion Table</a> 的頁面，看到下面這個畫面：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/vFIqF2IuSOuhKhR0mKUt_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.33.31.png" alt="螢幕快照 2015-10-15 上午10.33.31.png"></p>
<p>接著選擇「CREATE A FUSION TABLE」，會看到這樣的畫面：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/QrsLmP03QNamaW0qrqjT_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.37.48.png" alt="螢幕快照 2015-10-15 上午10.37.48.png"></p>
<p>現在試著把剛剛的 csv 直接餵給他，看看會發生什麼事
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/Wf4cjXTQTrCXlw0UDWMl_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.05.47.png" alt="螢幕快照 2015-10-15 上午10.05.47.png"></p>
<p>如果不意外，你應該會得到像這樣的亂碼資料 XDDDDD
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/AUqRTPyFQX29z5TdLGaM_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.06.19.png" alt="螢幕快照 2015-10-15 上午10.06.19.png"></p>
<p>不過沒關係，山不轉路轉，檔案格式當然也可以轉。只是需要多走幾步路。
這次我們將原始的 csv 資料先丟到 <a href="http://www.google.com/intl/zh-TW_tw/sheets/about/" target="_blank" rel="noreferrer">Google Sheet</a> 內，請他將我們的資料轉成正確的編碼。 在建立一個新的 Google Sheet 之後，我們選擇 File &gt; Import 將剛剛的 csv 檔案匯入：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/fNpq4j4rTEuynwQAcxOO_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.07.40.png" alt="螢幕快照 2015-10-15 上午10.07.40.png"></p>
<p>記得一樣要選擇 Comma，因為是透過逗點分隔的資料格式。
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/bbUWtz7RXqdpZsizK5Lw_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.08.07.png" alt="螢幕快照 2015-10-15 上午10.08.07.png"></p>
<p>然後就會得到正確編碼後的資料囉。<del>不過糖廍里一樣 GG...因為原始資料就 GG 了... orz</del>
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/wYMPpnlHTRmrgoywrKGc_%E6%9C%AA%E5%91%BD%E5%90%8D.png" alt="未命名.png"></p>
<p>ok, 到了這裡我們已經有一份正確的資料，這時再回到 Funsion Table，我們選擇 「Google Spreadsheets」，然後將剛剛建立的 Google Sheet 匯入進來:
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/QrsLmP03QNamaW0qrqjT_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.37.48.png" alt="螢幕快照 2015-10-15 上午10.37.48.png"></p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/66zfnteRQy6L50MJQUZ1_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.11.18.png" alt="螢幕快照 2015-10-15 上午10.11.18.png"></p>
<p>匯入成功後，Funsion Table 會出現這樣的畫面：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/pDQ7qI4WSDub29jPiglU_%E6%9C%AA%E5%91%BD%E5%90%8D.png" alt="未命名.png"></p>
<p>這個時候，因為我們要製作地圖，所以要告訴 Funsion Table 地點的欄位不是單純的字串，而是用來表示地理資訊的資料。點選 Edit &gt; Change columns：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/thibCn9zRMO435hQgjFd_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.11.57.png" alt="螢幕快照 2015-10-15 上午10.11.57.png"></p>
<p>然後將發生地點的 Type 改成 Location，然後點上面藍色的 Save 儲存。
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/TK4RPNQtQNiXcQBclDeu_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.12.09.png" alt="螢幕快照 2015-10-15 上午10.12.09.png"></p>
<p>資料都準備好了，然後我們建立地圖：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/csg65yBiQIyKcj1PAnNk_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.58.53.png" alt="螢幕快照 2015-10-15 上午10.58.53.png"></p>
<p>不意外的話你應該會看到這樣的畫面，是因為我們傳入的是地址的文字資訊， Google 需要將它轉為經緯度後才能對應到地圖上：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/JJNbZ5DRRmehqBdoDytm_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.13.03.png" alt="螢幕快照 2015-10-15 上午10.13.03.png"></p>
<p>然後等待一段時間後，Funsion Table 就會為我們產生地圖，像這樣：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/7cjQA7USzGy7nGrQRLDr_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8810.20.02.png" alt="螢幕快照 2015-10-15 上午10.20.02.png"></p>
<p>除了地點標示外，他也提供了熱圖 (Heat Map) 的呈現，試著拉拉旁邊的捲軸調整參數吧：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/304787/wMQb7zI8R72jFxu7a3g4_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-10-15%20%E4%B8%8A%E5%8D%8811.03.53.png" alt="螢幕快照 2015-10-15 上午11.03.53.png"></p>
<p>當然你也可以做好的地圖 share 出來，像這樣：</p>
<iframe width="800" height="400" scrolling="no" frameborder="no" src="https://www.google.com/fusiontables/embedviz?q=select+col3+from+1sAGtul5gWv1_yRpPTYI1ZvPaHQRPDod-B0MbOeS_&amp;viz=MAP&amp;h=false&amp;lat=25.05814459656792&amp;lng=121.63429174086912&amp;t=1&amp;z=12&amp;l=col3&amp;y=3&amp;tmplt=4&amp;hml=GEOCODABLE"></iframe>
<p>透過 Fusion Table 我們可以不用寫任何的程式碼就生成一份資訊地圖，很簡單吧 😃</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>Google Map</category>
            <category>google map</category>
            <category>Fusion Table</category>
        </item>
        <item>
            <title><![CDATA[利用 Google Map 檢視台北市降雨淹水模擬圖]]></title>
            <link>https://kurohsu.dev/notes/use-google-map-view-rainfall-simulation-of-flooding-in-taipei-city-map.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/use-google-map-view-rainfall-simulation-of-flooding-in-taipei-city-map.html</guid>
            <pubDate>Tue, 29 Sep 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="利用-google-map-檢視台北市降雨淹水模擬圖" tabindex="-1">利用 Google Map 檢視台北市降雨淹水模擬圖 <a class="header-anchor" href="#利用-google-map-檢視台北市降雨淹水模擬圖" aria-label="Permalink to “利用 Google Map 檢視台北市降雨淹水模擬圖”">&#8203;</a></h1>
<p>看到前兩天的新聞: <a href="https://tw.news.yahoo.com/%E6%9F%AF%E6%96%87%E5%93%B2%E6%83%B3%E5%85%AC%E9%96%8B%E6%98%93%E6%B7%B9%E6%B0%B4%E5%9C%B0%E5%8D%80-%E5%8C%97%E5%B8%82%E5%BA%9C%E5%B7%B2%E4%B8%8A%E7%B6%B2-115016390.html" title="柯文哲想公開易淹水地區 北市府已上網" target="_blank" rel="noreferrer">柯文哲想公開易淹水地區 北市府已上網</a>，又剛好有前輩寫了一篇 <a href="http://gis.rchss.sinica.edu.tw/qgis/?p=3221" title="利用QGIS檢視台北市降雨淹水模擬圖" target="_blank" rel="noreferrer">利用QGIS檢視台北市降雨淹水模擬圖</a>，<del>雖然身處房仲業，但身為有良心的開發者不能只想著房價</del>，心想應也可透過 Google Map 來呈現，於是試了一下，順便寫篇記錄。</p>
<p>首先從<a href="http://data.taipei/opendata/datalist/datasetMeta?oid=fa1e8012-ebb4-473b-888e-97f9a9ce365e" title="臺北市政府資料開放平台" target="_blank" rel="noreferrer">臺北市政府資料開放平台</a>將所需的資料一一下載下來，格式是 kmz (其實就是 zip 壓縮)。在解壓縮後可以得到 doc.kml 檔案。 接著，雖然 Google maps API 有提供 <a href="https://developers.google.com/maps/documentation/javascript/examples/layer-kml" title="Google maps API KML Layers" target="_blank" rel="noreferrer">KML Layers</a> 的圖層嵌套，但轉出來的 kml 檔似乎要稍作修改後才能透過 KML Layers 套用在 Google map 上，這裏我選擇另一種做法: 將 kml 轉為 geoJSON 後使用。</p>
<p>前面說到要將 kml 檔案轉為 geoJSON，那麼該如何轉換格式呢？
幸好 mapbox 提供了 <a href="https://github.com/mapbox/togeojson" target="_blank" rel="noreferrer">togeojson</a> 這套工具，透過它提供的 <code>toGeoJSON.kml(doc)</code> 就可以輕鬆地將它轉為 geoJSON 的格式了。</p>
<p>有關 GeoJSON 如何輸出至 Google Map 可以參考小弟之前的 post：<a href="http://kuro.tw/posts/2015/04/28/through-the-google-maps-api-geojson-data" target="_blank" rel="noreferrer">透過 Google Maps API 處理 GeoJSON 資料</a>，這裏就不再贅述。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="利用-google-map-檢視台北市降雨淹水模擬圖" tabindex="-1">利用 Google Map 檢視台北市降雨淹水模擬圖 <a class="header-anchor" href="#利用-google-map-檢視台北市降雨淹水模擬圖" aria-label="Permalink to “利用 Google Map 檢視台北市降雨淹水模擬圖”">&#8203;</a></h1>
<p>看到前兩天的新聞: <a href="https://tw.news.yahoo.com/%E6%9F%AF%E6%96%87%E5%93%B2%E6%83%B3%E5%85%AC%E9%96%8B%E6%98%93%E6%B7%B9%E6%B0%B4%E5%9C%B0%E5%8D%80-%E5%8C%97%E5%B8%82%E5%BA%9C%E5%B7%B2%E4%B8%8A%E7%B6%B2-115016390.html" title="柯文哲想公開易淹水地區 北市府已上網" target="_blank" rel="noreferrer">柯文哲想公開易淹水地區 北市府已上網</a>，又剛好有前輩寫了一篇 <a href="http://gis.rchss.sinica.edu.tw/qgis/?p=3221" title="利用QGIS檢視台北市降雨淹水模擬圖" target="_blank" rel="noreferrer">利用QGIS檢視台北市降雨淹水模擬圖</a>，<del>雖然身處房仲業，但身為有良心的開發者不能只想著房價</del>，心想應也可透過 Google Map 來呈現，於是試了一下，順便寫篇記錄。</p>
<p>首先從<a href="http://data.taipei/opendata/datalist/datasetMeta?oid=fa1e8012-ebb4-473b-888e-97f9a9ce365e" title="臺北市政府資料開放平台" target="_blank" rel="noreferrer">臺北市政府資料開放平台</a>將所需的資料一一下載下來，格式是 kmz (其實就是 zip 壓縮)。在解壓縮後可以得到 doc.kml 檔案。 接著，雖然 Google maps API 有提供 <a href="https://developers.google.com/maps/documentation/javascript/examples/layer-kml" title="Google maps API KML Layers" target="_blank" rel="noreferrer">KML Layers</a> 的圖層嵌套，但轉出來的 kml 檔似乎要稍作修改後才能透過 KML Layers 套用在 Google map 上，這裏我選擇另一種做法: 將 kml 轉為 geoJSON 後使用。</p>
<p>前面說到要將 kml 檔案轉為 geoJSON，那麼該如何轉換格式呢？
幸好 mapbox 提供了 <a href="https://github.com/mapbox/togeojson" target="_blank" rel="noreferrer">togeojson</a> 這套工具，透過它提供的 <code>toGeoJSON.kml(doc)</code> 就可以輕鬆地將它轉為 geoJSON 的格式了。</p>
<p>有關 GeoJSON 如何輸出至 Google Map 可以參考小弟之前的 post：<a href="http://kuro.tw/posts/2015/04/28/through-the-google-maps-api-geojson-data" target="_blank" rel="noreferrer">透過 Google Maps API 處理 GeoJSON 資料</a>，這裏就不再贅述。</p>
<hr>
<p>值得一提的是，透過 Google Maps Javascript API 輸出的 GeoJSON 預設的樣式都是黑色粗線，想要修改樣式的話可以透過 <code>setStyle</code> 來做處理，像這樣可以針對 name 是 0.3 的時候，我們輸出樣式為藍色的線，如果是 0.3~1.0 的話，則是輸出為綠色的線段，當然也可以針對填色與透明度等等做設定。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">dataMap.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setStyle</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">feature</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( feature.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getProperty</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'name'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '0.3'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { fillOpacity: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.35</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, fillColor: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#0070FF'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, strokeWeight: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, strokeColor: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#0070FF'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, strokeOpacity: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( feature.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getProperty</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'name'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '0.3~1.0'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { fillOpacity: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.35</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, fillColor: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#54FF00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, strokeWeight: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, strokeColor: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#54FF00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, strokeOpacity: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>結果呈現像這樣：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/302535/lNUGoyd7RB2MOsx8i1cw_image.png" alt="image.png"></p>
<p>這裏也有 Online Demo: <a href="http://kurotanshi.github.io/TPEDisasterSummary/rain/rain_tp_map.html" target="_blank" rel="noreferrer">http://kurotanshi.github.io/TPEDisasterSummary/rain/rain_tp_map.html</a></p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>OpenData</category>
            <category>google map</category>
            <category>opendata</category>
        </item>
        <item>
            <title><![CDATA[筆記 JavaScript 變數宣告與作用域]]></title>
            <link>https://kurohsu.dev/notes/note-javascript-variables-declared-with-the-scope-scope.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/note-javascript-variables-declared-with-the-scope-scope.html</guid>
            <pubDate>Wed, 08 Jul 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="筆記-javascript-變數宣告與作用域" tabindex="-1">筆記 JavaScript 變數宣告與作用域 <a class="header-anchor" href="#筆記-javascript-變數宣告與作用域" aria-label="Permalink to “筆記 JavaScript 變數宣告與作用域”">&#8203;</a></h1>
<p>大家都知道，JavaScript 的變數有其作用域的範圍，若使用前未經 var 宣告，就會自動變成全域變數 (global variable)，而在其 code block 內宣告的變數也只有該 code block 內可以使用。</p>
<p>這次的問題，其實很久以前在 tonyQ 的聚會上就聽他說過了，<del>只是沒想到還真的會遇到 XDDDD</del></p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<h1 id="筆記-javascript-變數宣告與作用域" tabindex="-1">筆記 JavaScript 變數宣告與作用域 <a class="header-anchor" href="#筆記-javascript-變數宣告與作用域" aria-label="Permalink to “筆記 JavaScript 變數宣告與作用域”">&#8203;</a></h1>
<p>大家都知道，JavaScript 的變數有其作用域的範圍，若使用前未經 var 宣告，就會自動變成全域變數 (global variable)，而在其 code block 內宣告的變數也只有該 code block 內可以使用。</p>
<p>這次的問題，其實很久以前在 tonyQ 的聚會上就聽他說過了，<del>只是沒想到還真的會遇到 XDDDD</del></p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a);		</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})();</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>上面的 code 很簡單，就是宣告一個全域變數 a，然後值為 1 ，因為是全域變數，所以在之後的匿名函式內可以使用它。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a);		</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 100</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})();</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>這次，在匿名函式內，我們另外宣告了一個 a，值為 100，因為其作用域的關係，所以 console 的結果會是 100。</p>
<p>接著，問題來了，如果我們在 <code>var a = 100;</code> 之前去取值，會發生什麼事呢？</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a);    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">//  ?</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a);    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 100</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})();</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><p>答案是，第一次的 console.log(a) 會印出 <strong>undefined</strong>，而第二次會出現 100。</p>
<p>因為在匿名函數獨立的 scope 內，不管 var 是放在最前面，或是最後一行，他的變數實體在該 code block 一開始就是新的了，也就是說，剛剛的 code 其實等同下面這段：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a);    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// undefined</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a);    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 100</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})();</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>所以第一次會印出 undefined 。</p>
<p>要怎麼排除這樣的問題呢，很簡單，要嘛一開始就不要取相同名稱，要嘛就透過參數的方式代入原本的變數，像這樣：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">_a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(_a);    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 100</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a);    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 100</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})(a);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div>]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>javascript</category>
        </item>
        <item>
            <title><![CDATA[Taipei D3-js Meetup 小聚分享心得]]></title>
            <link>https://kurohsu.dev/notes/taipei-d3js-meetup-gathering-to-share-experiences.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/taipei-d3js-meetup-gathering-to-share-experiences.html</guid>
            <pubDate>Thu, 18 Jun 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="taipei-d3-js-meetup-小聚分享心得" tabindex="-1">Taipei D3-js Meetup 小聚分享心得 <a class="header-anchor" href="#taipei-d3-js-meetup-小聚分享心得" aria-label="Permalink to “Taipei D3-js Meetup 小聚分享心得”">&#8203;</a></h1>
<p>這次我在 <a href="http://www.meetup.com/Taipei-D3-js-Meetup/" title="Taipei D3-js Meetup" target="_blank" rel="noreferrer">Taipei D3-js Meetup</a> (現在似乎改叫 Visual Thursday) 分享的 Talk 是有關於地理視覺化的簡介，雖然題目聽起來很學術，但其實內容是我這陣子對於 Web GIS 以及地圖視覺化的一些摸索心得分享。</p>
<p>感謝台灣微軟提供場地 XD</p>
<p>下面是我的投影片</p>
<iframe src="https://www.slideshare.net/slideshow/embed_code/key/mhzctMFXFoHMJn" width="476" height="400" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="margin-bottom:1em;"></iframe>
]]></description>
            <content:encoded><![CDATA[<h1 id="taipei-d3-js-meetup-小聚分享心得" tabindex="-1">Taipei D3-js Meetup 小聚分享心得 <a class="header-anchor" href="#taipei-d3-js-meetup-小聚分享心得" aria-label="Permalink to “Taipei D3-js Meetup 小聚分享心得”">&#8203;</a></h1>
<p>這次我在 <a href="http://www.meetup.com/Taipei-D3-js-Meetup/" title="Taipei D3-js Meetup" target="_blank" rel="noreferrer">Taipei D3-js Meetup</a> (現在似乎改叫 Visual Thursday) 分享的 Talk 是有關於地理視覺化的簡介，雖然題目聽起來很學術，但其實內容是我這陣子對於 Web GIS 以及地圖視覺化的一些摸索心得分享。</p>
<p>感謝台灣微軟提供場地 XD</p>
<p>下面是我的投影片</p>
<iframe src="https://www.slideshare.net/slideshow/embed_code/key/mhzctMFXFoHMJn" width="476" height="400" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="margin-bottom:1em;"></iframe>
<hr>
<p>這次幾個 Demo 的原始碼都放在 Github 上： <a href="https://github.com/kurotanshi/VisualThursday_demo" target="_blank" rel="noreferrer">https://github.com/kurotanshi/VisualThursday_demo</a>
有興趣的朋友可以自行下載研究。</p>
<p>想看 Online Demo 的也可以直接到這： <a href="http://kurotanshi.github.io/VisualThursday_demo/" target="_blank" rel="noreferrer">http://kurotanshi.github.io/VisualThursday_demo/</a></p>
<p>雖然都是以 Google Map 為範例，但是大部份的圖資系統如 leaflet.js / MapBox 等都是通用的，尤其是 <a href="http://turfjs.org/" title="http://turfjs.org/" target="_blank" rel="noreferrer">Turf.js</a> 這個地理資訊分析的工具，十分強大，可以直接針對 geojson 做運算，也可以透過 node 在後端執行運算。至今我還在摸索它的功能，也許未來更加熟練後可以再來分享給大家。</p>
<p>謝謝所有在端午連假前仍願意來參與活動的朋友，大家端午節快樂 😃</p>
<p>最後，我們還有很多專案想實現，歡迎有志之士加入！</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>D3-js</category>
            <category>opendata</category>
        </item>
        <item>
            <title><![CDATA[JS Note TWD97 標轉換爲 經緯度]]></title>
            <link>https://kurohsu.dev/notes/js-note-twd97-convert-to-longitude-and-latitude.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/js-note-twd97-convert-to-longitude-and-latitude.html</guid>
            <pubDate>Thu, 11 Jun 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="js-note-twd97-標轉換爲-經緯度" tabindex="-1">JS Note TWD97 標轉換爲 經緯度 <a class="header-anchor" href="#js-note-twd97-標轉換爲-經緯度" aria-label="Permalink to “JS Note TWD97 標轉換爲 經緯度”">&#8203;</a></h1>
<p>因為從臺北市政府資料開放平台 API 拿到的資料是 TWD97 座標格式，所以我們必須要將它轉成經緯度後才能套用至 Google Map 使用。網路上查了很多資料，最後找到最親民的是米蟲大的 PHP 版本，於是就改成寫我最熟悉的 JS。</p>
<p>reference:
<a href="http://blog.xuite.net/vexed/tech/53749528-TWD97+%E5%BA%A7%E6%A8%99%E8%BD%89%E7%B6%93%E7%B7%AF%E5%BA%A6" target="_blank" rel="noreferrer">Vexed's Blog - TWD97 座標轉經緯度</a></p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> twd97_to_latlng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">$x</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">$y</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<h1 id="js-note-twd97-標轉換爲-經緯度" tabindex="-1">JS Note TWD97 標轉換爲 經緯度 <a class="header-anchor" href="#js-note-twd97-標轉換爲-經緯度" aria-label="Permalink to “JS Note TWD97 標轉換爲 經緯度”">&#8203;</a></h1>
<p>因為從臺北市政府資料開放平台 API 拿到的資料是 TWD97 座標格式，所以我們必須要將它轉成經緯度後才能套用至 Google Map 使用。網路上查了很多資料，最後找到最親民的是米蟲大的 PHP 版本，於是就改成寫我最熟悉的 JS。</p>
<p>reference:
<a href="http://blog.xuite.net/vexed/tech/53749528-TWD97+%E5%BA%A7%E6%A8%99%E8%BD%89%E7%B6%93%E7%B7%AF%E5%BA%A6" target="_blank" rel="noreferrer">Vexed's Blog - TWD97 座標轉經緯度</a></p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> twd97_to_latlng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">$x</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">$y</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pow </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.pow, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">M_PI</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">PI</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> sin </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.sin, cos </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.cos, tan </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.tan;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 6378137.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, $b </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 6356752.314245</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $lng0 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 121</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> M_PI</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> /</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 180</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, $k0 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0.9999</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, $dx </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 250000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, $dy </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $e </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($b, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($a, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $dx;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $dy;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $M </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $k0;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $mu </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $M </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ($a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1.0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 4.0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 3</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">4</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 64.0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 5</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">6</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 256.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $e1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1.0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1.0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1.0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1.0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $J1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $e1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 27</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 32.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $J2 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">21</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 16</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 55</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">4</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 32.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $J3 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">151</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 96.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $J4 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1097</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">4</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 512.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $fp </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $mu </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $J1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sin</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $mu) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $J2 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sin</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">4</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $mu) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $J3 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sin</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">6</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $mu) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $J4 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> sin</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">8</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $mu);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $e2 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(($e </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $b), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $C1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e2 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> cos</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($fp), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $T1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">tan</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($fp), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $R1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sin</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($fp), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)), (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3.0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> /</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $N1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $a </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($e, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sin</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($fp), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $D </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ($N1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $k0);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $N1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> tan</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($fp) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $R1;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q2 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($D, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q3 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">5</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 3</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $T1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 10</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $C1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 4</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($C1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 9</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $e2) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($D, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">4</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 24.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q4 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">61</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 90</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $T1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 298</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $C1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 45</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($T1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 3</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($C1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 252</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $e2) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($D, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">6</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 720.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $lat </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $fp </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ($Q2 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q3 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q4);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q5 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $D;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q6 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $T1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $C1) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($D, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 6</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q7 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">5</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $C1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 28</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $T1 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 3</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($C1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 8</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $e2 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 24</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($T1, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($D, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 120.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $lng </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $lng0 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ($Q5 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q6 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $Q7) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> cos</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($fp);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $lat </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ($lat </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 180</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> M_PI</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $lng </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ($lng </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 180</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> M_PI</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    lat: $lat,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    lng: $lng</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br></div></div>]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>GIS</category>
            <category>twd97</category>
            <category>gis</category>
            <category>opendata</category>
        </item>
        <item>
            <title><![CDATA[在 Google Map 加入 D3 圖像 - 2]]></title>
            <link>https://kurohsu.dev/notes/added-to-the-google-map-images-d3-2.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/added-to-the-google-map-images-d3-2.html</guid>
            <pubDate>Wed, 20 May 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="在-google-map-加入-d3-圖像-2" tabindex="-1">在 Google Map 加入 D3 圖像 - 2 <a class="header-anchor" href="#在-google-map-加入-d3-圖像-2" aria-label="Permalink to “在 Google Map 加入 D3 圖像 - 2”">&#8203;</a></h1>
<p><a href="http://kuro.tw/posts/2015/05/20/join-the-d3-in-google-map-image" title="在 Google Map 加入 D3 圖像 " target="_blank" rel="noreferrer">上一篇</a>提到了如何在 Google Map 裡面加入 D3 的圖像，這次我們實際將資料套進去吧。</p>
<p>資料來源是上篇提到的<a href="http://data.taipei/opendata/datalist/datasetMeta?oid=1d71c478-205f-42c5-8386-35f86d74fdd1" target="_blank" rel="noreferrer">臺北捷運各站進出量統計</a>的統計資料，因為台北市政府開放平台並沒有提供 CORS (跨來源資源共享)的服務，沒關係，我們直接將資料下載存成 json 檔案即可。 (範例為 2015/4 進出站人數)</p>
<p>按照慣例，先看結果 - Demo: <a href="http://jsbin.com/wexiva/3/" target="_blank" rel="noreferrer">http://jsbin.com/wexiva/3/</a>
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/276658/4DhTbWRwSvujvsxZC2Z5_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-05-20%20%E4%B8%8B%E5%8D%8811.54.46.png" alt="螢幕快照 2015-05-20 下午11.54.46.png"></p>
<p>藍色的是進站人數，橘色的是出站人數。可以看出各站在 4/1 的進出站人數相當平均。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="在-google-map-加入-d3-圖像-2" tabindex="-1">在 Google Map 加入 D3 圖像 - 2 <a class="header-anchor" href="#在-google-map-加入-d3-圖像-2" aria-label="Permalink to “在 Google Map 加入 D3 圖像 - 2”">&#8203;</a></h1>
<p><a href="http://kuro.tw/posts/2015/05/20/join-the-d3-in-google-map-image" title="在 Google Map 加入 D3 圖像 " target="_blank" rel="noreferrer">上一篇</a>提到了如何在 Google Map 裡面加入 D3 的圖像，這次我們實際將資料套進去吧。</p>
<p>資料來源是上篇提到的<a href="http://data.taipei/opendata/datalist/datasetMeta?oid=1d71c478-205f-42c5-8386-35f86d74fdd1" target="_blank" rel="noreferrer">臺北捷運各站進出量統計</a>的統計資料，因為台北市政府開放平台並沒有提供 CORS (跨來源資源共享)的服務，沒關係，我們直接將資料下載存成 json 檔案即可。 (範例為 2015/4 進出站人數)</p>
<p>按照慣例，先看結果 - Demo: <a href="http://jsbin.com/wexiva/3/" target="_blank" rel="noreferrer">http://jsbin.com/wexiva/3/</a>
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/276658/4DhTbWRwSvujvsxZC2Z5_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-05-20%20%E4%B8%8B%E5%8D%8811.54.46.png" alt="螢幕快照 2015-05-20 下午11.54.46.png"></p>
<p>藍色的是進站人數，橘色的是出站人數。可以看出各站在 4/1 的進出站人數相當平均。</p>
<hr>
<p>因為這次抓取的資料比較多，所以程式也稍微複雜一點，不過沒關係，概念還是很簡單的。
不囉唆直接看 code.</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> map;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> overlay </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">OverlayView</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> sta_in </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [], sta_out </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [], mrtData;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 讀取資料, sta_in = 2015 年四月進站人數, sta_out = 2015 年四月出站人數.</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 資料來源: http://data.taipei/opendata/datalist/datasetMeta?oid=1d71c478-205f-42c5-8386-35f86d74fdd1</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">csv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://dl.dropboxusercontent.com/u/12537630/mrt.csv"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  mrtData </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> data;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 進站</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  d3</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">json</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://dl.dropboxusercontent.com/u/12537630/mrt-in-april.json"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">json</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    sta_in </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> json.result.results;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( sta_in.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ></span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> sta_out.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ></span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">drawMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(); }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 出站</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  d3</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">json</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://dl.dropboxusercontent.com/u/12537630/mrt-out-april.json"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">error</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">json</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    sta_out </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> json.result.results;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( sta_in.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ></span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> sta_out.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ></span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">drawMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(); }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> drawMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 設定圓餅圖長寬, 半徑</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> width </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 35</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, height </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 40</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, radius </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">min</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(width, height) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 透過 d3.scale.category10() 生成顏色</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> color </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.scale.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">category10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">		// d3.layout.pie()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">		var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pie </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.layout.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pie</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 設定圓餅內外層半徑, 這裏內層設 0.</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> arc </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.svg.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">arc</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">innerRadius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">outerRadius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(radius);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    overlay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">onAdd</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> layer </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getPanes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().overlayLayer).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"div"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"class"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"stations"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        overlay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">draw</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> projection </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getProjection</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                padding </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 16</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">            // 針對每一筆捷運站增加 marker</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> marker </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> layer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">selectAll</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"svg"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">entries</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(mrtData))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">each</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(transform)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">enter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"svg"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">each</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(transform)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                    'class'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"marker"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                    "width"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: width,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                    "height"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: height,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                    "transform"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"translate("</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> width </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ","</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> height </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ")"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">            // 將取得的進出站資料透過 .data( pie([ 進站人數, 出站人數 ]) ) 指定到圓餅圖中。</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">            // sta_in[0] 代表 4/1 進站人數, sta_out[0] 代表 4/1 出站人數.</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> g </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> marker.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">selectAll</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"g"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">i</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">                    return</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> pie</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parseInt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(sta_in[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">][d.value.name].</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">','</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parseInt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(sta_out[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">][d.value.name].</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">','</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)]);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                })</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">enter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"g"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">            // 著色</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            g.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"path"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                  "fill"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">i</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) { </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(i); },</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                  "d"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, arc,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                  "transform"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"translate("</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> width </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ","</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> height </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ")"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">            // 加入標籤</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            marker.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"text"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"x"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, padding </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 7</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"y"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, padding)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"dy"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">".31em"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">                    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d.value.name;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> transform</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                d </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LatLng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(d.value.lat, d.value.lng);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                d </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> projection.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">fromLatLngToDivPixel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(d);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">                return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"left"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, (d.x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> padding) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "px"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"top"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, (d.y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> padding) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "px"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    overlay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(map);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br><span class="line-number">55</span><br><span class="line-number">56</span><br><span class="line-number">57</span><br><span class="line-number">58</span><br><span class="line-number">59</span><br><span class="line-number">60</span><br><span class="line-number">61</span><br><span class="line-number">62</span><br><span class="line-number">63</span><br><span class="line-number">64</span><br><span class="line-number">65</span><br><span class="line-number">66</span><br><span class="line-number">67</span><br><span class="line-number">68</span><br><span class="line-number">69</span><br><span class="line-number">70</span><br><span class="line-number">71</span><br><span class="line-number">72</span><br><span class="line-number">73</span><br><span class="line-number">74</span><br><span class="line-number">75</span><br><span class="line-number">76</span><br><span class="line-number">77</span><br><span class="line-number">78</span><br><span class="line-number">79</span><br><span class="line-number">80</span><br><span class="line-number">81</span><br><span class="line-number">82</span><br><span class="line-number">83</span><br><span class="line-number">84</span><br><span class="line-number">85</span><br><span class="line-number">86</span><br><span class="line-number">87</span><br><span class="line-number">88</span><br><span class="line-number">89</span><br><span class="line-number">90</span><br><span class="line-number">91</span><br><span class="line-number">92</span><br><span class="line-number">93</span><br><span class="line-number">94</span><br><span class="line-number">95</span><br><span class="line-number">96</span><br><span class="line-number">97</span><br><span class="line-number">98</span><br><span class="line-number">99</span><br><span class="line-number">100</span><br><span class="line-number">101</span><br></div></div><h4 id="本系列文章列表" tabindex="-1">本系列文章列表: <a class="header-anchor" href="#本系列文章列表" aria-label="Permalink to “本系列文章列表:”">&#8203;</a></h4>
<ul>
<li><a href="http://kuro.tw/posts/2015/05/20/join-the-d3-in-google-map-image" target="_blank" rel="noreferrer">在 Google Map 加入 D3 圖像 (1)</a></li>
<li><a href="http://kuro.tw/posts/2015/05/20/added-to-the-google-map-images-d3-2" target="_blank" rel="noreferrer">在 Google Map 加入 D3 圖像 (2)</a></li>
</ul>
<hr>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>D3-js</category>
            <category>google map</category>
            <category>d3-js</category>
        </item>
        <item>
            <title><![CDATA[在 Google Map 加入 D3 圖像]]></title>
            <link>https://kurohsu.dev/notes/join-the-d3-in-google-map-image.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/join-the-d3-in-google-map-image.html</guid>
            <pubDate>Tue, 19 May 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="在-google-map-加入-d3-圖像" tabindex="-1">在 Google Map 加入 D3 圖像 <a class="header-anchor" href="#在-google-map-加入-d3-圖像" aria-label="Permalink to “在 Google Map 加入 D3 圖像”">&#8203;</a></h1>
<p>因為昨天 <a href="http://data.taipei/" target="_blank" rel="noreferrer">台北市政府開放平台</a> 開始提供 <a href="http://data.taipei/opendata/datalist/datasetMeta?oid=1d71c478-205f-42c5-8386-35f86d74fdd1" target="_blank" rel="noreferrer">臺北捷運各站進出量統計</a> 的統計資料，所以就在思考可以利用這份資料做什麼應用，第一個想到的就是能否透過 D3 與地圖的共同呈現，當然就從我最熟悉的 Google Map 開始。</p>
<p>不過這篇文章只會介紹到如何在 Google Map 加入 D3 (SVG) 圖像。
等我將捷運各站資訊加入後也許會還有下回，哈哈哈。</p>
<p>先看結果 - Demo: <a href="http://output.jsbin.com/wexiva/1/" target="_blank" rel="noreferrer">http://output.jsbin.com/wexiva/1/</a>
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/276547/NyZaTlSTDuH8v6DRzWAY_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-05-19%20%E4%B8%8B%E5%8D%8811.34.22.png" alt="螢幕快照 2015-05-19 下午11.34.22.png"></p>
]]></description>
            <content:encoded><![CDATA[<h1 id="在-google-map-加入-d3-圖像" tabindex="-1">在 Google Map 加入 D3 圖像 <a class="header-anchor" href="#在-google-map-加入-d3-圖像" aria-label="Permalink to “在 Google Map 加入 D3 圖像”">&#8203;</a></h1>
<p>因為昨天 <a href="http://data.taipei/" target="_blank" rel="noreferrer">台北市政府開放平台</a> 開始提供 <a href="http://data.taipei/opendata/datalist/datasetMeta?oid=1d71c478-205f-42c5-8386-35f86d74fdd1" target="_blank" rel="noreferrer">臺北捷運各站進出量統計</a> 的統計資料，所以就在思考可以利用這份資料做什麼應用，第一個想到的就是能否透過 D3 與地圖的共同呈現，當然就從我最熟悉的 Google Map 開始。</p>
<p>不過這篇文章只會介紹到如何在 Google Map 加入 D3 (SVG) 圖像。
等我將捷運各站資訊加入後也許會還有下回，哈哈哈。</p>
<p>先看結果 - Demo: <a href="http://output.jsbin.com/wexiva/1/" target="_blank" rel="noreferrer">http://output.jsbin.com/wexiva/1/</a>
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/276547/NyZaTlSTDuH8v6DRzWAY_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-05-19%20%E4%B8%8B%E5%8D%8811.34.22.png" alt="螢幕快照 2015-05-19 下午11.34.22.png"></p>
<hr>
<p>整個程式非常簡單，首先在 Google map 加入一個 <code>google.maps.OverlayView()</code>，然後透過 <code>d3.csv</code> 載入資料。在繪製 marker (圓點) 的時候，透過自訂的 <code>transform function</code> 去指定它的座標就完成了。</p>
<p>相關程式如下:</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 捷運各站經緯度資訊: https://dl.dropboxusercontent.com/u/12537630/mrt.csv</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 資料來源: https://github.com/repeat/taipei-metro-stations</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">csv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://dl.dropboxusercontent.com/u/12537630/mrt.csv"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> overlay </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">OverlayView</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 新增 OverlayView 到 google map</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  overlay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">onAdd</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> layer </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getPanes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().overlayLayer).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"div"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"class"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"stations"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    overlay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">draw</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> projection </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getProjection</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(), padding </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 16</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> marker </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> layer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">selectAll</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"svg"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">entries</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(data))</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">each</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(transform)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">enter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"svg:svg"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">each</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(transform)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"class"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"marker"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 加入圓點</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      marker.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"svg:circle"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"r"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">6</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"cx"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, padding)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"cy"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, padding);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 加入標籤</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      marker.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"svg:text"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"x"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, padding </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 7</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"y"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, padding)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"dy"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">".31em"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">          .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) { </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d.value.name; });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // transform function. 指定每個點的座標.</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> transform</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        d </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LatLng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(d.value.lat, d.value.lng);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        d </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> projection.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">fromLatLngToDivPixel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(d);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"left"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, (d.x </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> padding) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "px"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"top"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, (d.y </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> padding) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "px"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 將 overlay 加入到 google 地圖</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  overlay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(map);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br></div></div><h4 id="參考" tabindex="-1">參考: <a class="header-anchor" href="#參考" aria-label="Permalink to “參考:”">&#8203;</a></h4>
<ul>
<li><a href="http://bl.ocks.org/mbostock/899711" target="_blank" rel="noreferrer">http://bl.ocks.org/mbostock/899711</a></li>
<li><a href="https://developers.google.com/maps/documentation/javascript/customoverlays?hl=zh-tw" target="_blank" rel="noreferrer">https://developers.google.com/maps/documentation/javascript/customoverlays?hl=zh-tw</a></li>
</ul>
<hr>
<h4 id="本系列文章列表" tabindex="-1">本系列文章列表: <a class="header-anchor" href="#本系列文章列表" aria-label="Permalink to “本系列文章列表:”">&#8203;</a></h4>
<ul>
<li><a href="http://kuro.tw/posts/2015/05/20/join-the-d3-in-google-map-image" target="_blank" rel="noreferrer">在 Google Map 加入 D3 圖像 (1)</a></li>
<li><a href="http://kuro.tw/posts/2015/05/20/added-to-the-google-map-images-d3-2" target="_blank" rel="noreferrer">在 Google Map 加入 D3 圖像 (2)</a></li>
</ul>
<hr>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>D3-js</category>
            <category>d3-js</category>
            <category>google map</category>
        </item>
        <item>
            <title><![CDATA[Modern WebConf 2015 與我的講題：D3 圖表優化二三事]]></title>
            <link>https://kurohsu.dev/notes/modern-webconf-2015-with-my-theme-d3-optimizing-two-or-three-things.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/modern-webconf-2015-with-my-theme-d3-optimizing-two-or-three-things.html</guid>
            <pubDate>Sun, 17 May 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="modern-webconf-2015-與我的講題-d3-圖表優化二三事" tabindex="-1">Modern WebConf 2015 與我的講題：D3 圖表優化二三事 <a class="header-anchor" href="#modern-webconf-2015-與我的講題-d3-圖表優化二三事" aria-label="Permalink to “Modern WebConf 2015 與我的講題：D3 圖表優化二三事”">&#8203;</a></h1>
<p>下面投影片是這次小弟在 Modern WebConf 2015 分享的主題，內容是有關 D3 開發的一些心得。</p>
<p>其實早先在準備的時候有些忐忑，像這種大拜拜的演講場合，很難預想聽眾的程度，講題太深擔心會眾無法吸收，太淺又怕對不起會眾的期待。 所以這次的內容安排了三分之一是從最基礎的 D3 data-driven 講起，再來才是開發心得與特性的介紹，期望不管是剛入門的朋友或是已經投入開發的老手都能從中獲得些什麼。因為我認為 Data-Driven 是 D3-js 的核心觀念之一，在瞭解如何將資料轉為 DOM / SVG 元件以後，剩下再去讀 D3 的 API 相信也能輕易上手。</p>
<p><a href="http://audrey.nu/-/2015/05/16/open-source-enlightenment-2015" title="開源之道 2015 " target="_blank" rel="noreferrer">「萬事萬物都有缺口，缺口就是光的入口」</a>，如果有什麼是我講的不對的地方，也歡迎 <del>用力打臉</del> 糾正，畢竟我都這麼厚臉皮出來分享了，讓小弟我有機會獲得正確答案應該不過分，哈哈哈。</p>
<p>最後，在這裡預告一下，感謝 <a href="http://www.meetup.com/Taipei-D3-js-Meetup/" title="Taipei D3-js Meetup" target="_blank" rel="noreferrer">Taipei D3-js Meetup</a> 的邀約，預計在六月份會有一場小聚的分享，題目還在構思當中。 如果你對 D3 相關議題也有興趣，歡迎與我交流。 😃</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="modern-webconf-2015-與我的講題-d3-圖表優化二三事" tabindex="-1">Modern WebConf 2015 與我的講題：D3 圖表優化二三事 <a class="header-anchor" href="#modern-webconf-2015-與我的講題-d3-圖表優化二三事" aria-label="Permalink to “Modern WebConf 2015 與我的講題：D3 圖表優化二三事”">&#8203;</a></h1>
<p>下面投影片是這次小弟在 Modern WebConf 2015 分享的主題，內容是有關 D3 開發的一些心得。</p>
<p>其實早先在準備的時候有些忐忑，像這種大拜拜的演講場合，很難預想聽眾的程度，講題太深擔心會眾無法吸收，太淺又怕對不起會眾的期待。 所以這次的內容安排了三分之一是從最基礎的 D3 data-driven 講起，再來才是開發心得與特性的介紹，期望不管是剛入門的朋友或是已經投入開發的老手都能從中獲得些什麼。因為我認為 Data-Driven 是 D3-js 的核心觀念之一，在瞭解如何將資料轉為 DOM / SVG 元件以後，剩下再去讀 D3 的 API 相信也能輕易上手。</p>
<p><a href="http://audrey.nu/-/2015/05/16/open-source-enlightenment-2015" title="開源之道 2015 " target="_blank" rel="noreferrer">「萬事萬物都有缺口，缺口就是光的入口」</a>，如果有什麼是我講的不對的地方，也歡迎 <del>用力打臉</del> 糾正，畢竟我都這麼厚臉皮出來分享了，讓小弟我有機會獲得正確答案應該不過分，哈哈哈。</p>
<p>最後，在這裡預告一下，感謝 <a href="http://www.meetup.com/Taipei-D3-js-Meetup/" title="Taipei D3-js Meetup" target="_blank" rel="noreferrer">Taipei D3-js Meetup</a> 的邀約，預計在六月份會有一場小聚的分享，題目還在構思當中。 如果你對 D3 相關議題也有興趣，歡迎與我交流。 😃</p>
<hr>
<p>無論如何，還是感謝每一位參與 Modern WebConf 的會眾、講師以及工作人員。辛苦了。</p>
<iframe src="//www.slideshare.net/slideshow/embed_code/key/NdKfJWerFlauo9" width="510" height="420" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> <div style="margin-bottom:5px"> <strong> <a href="//www.slideshare.net/kurotanshi/d3-48180820" title="[Modern WebConf 2015] D3 圖表優化二三事" target="_blank">[Modern WebConf 2015] D3 圖表優化二三事</a> </strong> from <strong><a href="//www.slideshare.net/kurotanshi" target="_blank">Kuro Hsu</a></strong> </div>
<div style="margin-top:2em;">偷偷在底下擺張 JavaScript 之父 Brendan Eich，與小弟 <del>(自稱 JS 之子)</del> 的合照炫耀一下 XD</div>
![IMG_8751.jpg](http://user-image.logdown.io/user/3292/blog/3340/post/276313/SIxIklSuRka61onbK0IK_IMG_8751.jpg)]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>ModernWebConf</category>
            <category>d3-js</category>
        </item>
        <item>
            <title><![CDATA[筆記 阻擋 android chrome 網頁下拉自動重整頁面]]></title>
            <link>https://kurohsu.dev/notes/note-blocking-android-chrome-page-drop-down-automatically-restructure-page.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/note-blocking-android-chrome-page-drop-down-automatically-restructure-page.html</guid>
            <pubDate>Mon, 11 May 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="筆記-阻擋-android-chrome-網頁下拉自動重整頁面" tabindex="-1">筆記 阻擋 android chrome 網頁下拉自動重整頁面 <a class="header-anchor" href="#筆記-阻擋-android-chrome-網頁下拉自動重整頁面" aria-label="Permalink to “筆記 阻擋 android chrome 網頁下拉自動重整頁面”">&#8203;</a></h1>
<p>早上開會時，同事提到了 Android 升級後，Chrome for Android 會出現網頁畫面在瀏覽器頂端時，往下拉會重整頁面的問題(<del>其實應該說是 Feature XD</del>)，因為這個新功能會造成網頁使用上的一些困擾，於是試了幾種方式，發現可以阻擋網頁重整的動作，趁還有印象就把它記錄下來。</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/275711/HX5wrVYQ8WW8gkKhK8rQ_2015-05-11%2015.12.02.jpg" alt="2015-05-11 15.12.02.jpg">
(就是這個小圈圈，看到它就代表網頁要重整了)</p>
<p>原理其實很簡單，就是在當 chrome 已經到達網頁頂端時，而且判斷 touchmove 是往下拉時，把事件阻擋掉，就可以了。</p>
<p>可以用 Android 試試看。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="筆記-阻擋-android-chrome-網頁下拉自動重整頁面" tabindex="-1">筆記 阻擋 android chrome 網頁下拉自動重整頁面 <a class="header-anchor" href="#筆記-阻擋-android-chrome-網頁下拉自動重整頁面" aria-label="Permalink to “筆記 阻擋 android chrome 網頁下拉自動重整頁面”">&#8203;</a></h1>
<p>早上開會時，同事提到了 Android 升級後，Chrome for Android 會出現網頁畫面在瀏覽器頂端時，往下拉會重整頁面的問題(<del>其實應該說是 Feature XD</del>)，因為這個新功能會造成網頁使用上的一些困擾，於是試了幾種方式，發現可以阻擋網頁重整的動作，趁還有印象就把它記錄下來。</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/275711/HX5wrVYQ8WW8gkKhK8rQ_2015-05-11%2015.12.02.jpg" alt="2015-05-11 15.12.02.jpg">
(就是這個小圈圈，看到它就代表網頁要重整了)</p>
<p>原理其實很簡單，就是在當 chrome 已經到達網頁頂端時，而且判斷 touchmove 是往下拉時，把事件阻擋掉，就可以了。</p>
<p>可以用 Android 試試看。</p>
<hr>
<p>Demo: <a href="http://jsbin.com/conuga/1/" target="_blank" rel="noreferrer">http://jsbin.com/conuga/1/</a></p>
<p>聽說 iOS 也有類似問題，不過我手邊沒有 iPhone，所以無法測試。 orz</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">window.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">addEventListener</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'load'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> isWindowTop </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> lastTouchY </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> touchStartHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">e</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (e.touches.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        lastTouchY </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> e.touches[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].clientY;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        isWindowTop </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (window.pageYOffset </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> touchMoveHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">e</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> touchY </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> e.touches[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].clientY;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> touchYmove </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> touchY </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> lastTouchY;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        lastTouchY </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> touchY;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (isWindowTop) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            isWindowTop </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">            // 阻擋移動事件</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (touchYmove </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">></span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                e.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">preventDefault</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">                return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    document.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">addEventListener</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'touchstart'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, touchStartHandler, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    document.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">addEventListener</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'touchmove'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, touchMoveHandler, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br></div></div>]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>android</category>
            <category>chrome</category>
            <category>mobile</category>
        </item>
        <item>
            <title><![CDATA[筆記 Shapefile to GeoJSON]]></title>
            <link>https://kurohsu.dev/notes/note-shapefile-to-geojson.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/note-shapefile-to-geojson.html</guid>
            <pubDate>Mon, 04 May 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="筆記-shapefile-to-geojson" tabindex="-1">筆記 Shapefile to GeoJSON <a class="header-anchor" href="#筆記-shapefile-to-geojson" aria-label="Permalink to “筆記 Shapefile to GeoJSON”">&#8203;</a></h1>
<p>因為每次轉檔都要查，索性把步驟記錄下來。</p>
<p>首先要安裝 GDAL (Geospatial Data Abstraction Library) 這個程式，
因為我是用 Mac ，所以在 terminal 輸入 <code>brew install gdal</code> 就可以了。</p>
<p>其他作業系統的安裝方式可詳閱 <a href="http://www.gdal.org/" target="_blank" rel="noreferrer">http://www.gdal.org/</a> 。</p>
<p>安裝好 gdal 之後，就可以透過 ogr2ogr 來執行轉檔，一樣在 terminal 輸入：</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="筆記-shapefile-to-geojson" tabindex="-1">筆記 Shapefile to GeoJSON <a class="header-anchor" href="#筆記-shapefile-to-geojson" aria-label="Permalink to “筆記 Shapefile to GeoJSON”">&#8203;</a></h1>
<p>因為每次轉檔都要查，索性把步驟記錄下來。</p>
<p>首先要安裝 GDAL (Geospatial Data Abstraction Library) 這個程式，
因為我是用 Mac ，所以在 terminal 輸入 <code>brew install gdal</code> 就可以了。</p>
<p>其他作業系統的安裝方式可詳閱 <a href="http://www.gdal.org/" target="_blank" rel="noreferrer">http://www.gdal.org/</a> 。</p>
<p>安裝好 gdal 之後，就可以透過 ogr2ogr 來執行轉檔，一樣在 terminal 輸入：</p>
<hr>
<p><code>ogr2ogr -f &quot;GeoJSON&quot; output.json source.shp</code></p>
<p>以臺北市政府開放資料的<strong>臺北市區界圖</strong> ( <a href="http://data.taipei/opendata/datalist/datasetMeta?oid=1601ef3a-c253-4988-b047-943d9e786143" target="_blank" rel="noreferrer">http://data.taipei/opendata/datalist/datasetMeta?oid=1601ef3a-c253-4988-b047-943d9e786143</a> ) 提供的 Shapefile 來說，因為它的坐標系統是 EPSG:3826(TWD97/121分帶)，我們需要把它轉換成 WGS84經緯度(EPSG:4326)，所以透過 <code>-s_srs</code> 與 <code>-t_srs</code> 來分別指定轉換前與轉換後的座標系統：</p>
<p><code>ogr2ogr -f &quot;GeoJSON&quot; -s_srs EPSG:3826 -t_srs EPSG:4326  output.json source.shp</code></p>
<p>轉換後的 geojson 直接輸出到 google map 上：
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/263637/UrqBEFSmTuazvdty255S_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-05-05%20%E4%B8%8B%E5%8D%883.47.07.png" alt="螢幕快照 2015-05-05 下午3.47.07.png"></p>
<p>就這樣。</p>
<p>[補充]
關於 GeoJson 送到 Google Map 的部分可以參考我的其他文章：
<a href="http://kuro.tw/posts/2015/04/28/through-the-google-maps-api-geojson-data" title="透過 Google Maps API 處理 GeoJSON 資料" target="_blank" rel="noreferrer">透過 Google Maps API 處理 GeoJSON 資料</a>
<a href="http://kuro.tw/posts/2015/09/30/use-google-map-view-rainfall-simulation-of-flooding-in-taipei-city-map" title="利用 Google Map 檢視台北市降雨淹水模擬圖" target="_blank" rel="noreferrer">利用 Google Map 檢視台北市降雨淹水模擬圖</a></p>
<p>又，如果需要轉換成 TopoJSON 的話，也可以參考這篇： <a href="http://blog.infographics.tw/2015/04/visualize-geographics-with-d3js/" title="視覺化實戰 － D3-js 地理區塊視覺化" target="_blank" rel="noreferrer">視覺化實戰 － D3-js 地理區塊視覺化</a></p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>GeoJSON</category>
        </item>
        <item>
            <title><![CDATA[snippet D3-js 甜甜圈圖 (donut chart) 的放大漸變效果]]></title>
            <link>https://kurohsu.dev/notes/snippet-d3js-donuts-chart-the-transition-effect.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/snippet-d3js-donuts-chart-the-transition-effect.html</guid>
            <pubDate>Mon, 04 May 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="snippet-d3-js-甜甜圈圖-donut-chart-的放大漸變效果" tabindex="-1">snippet D3-js 甜甜圈圖 (donut chart) 的放大漸變效果 <a class="header-anchor" href="#snippet-d3-js-甜甜圈圖-donut-chart-的放大漸變效果" aria-label="Permalink to “snippet D3-js 甜甜圈圖 (donut chart) 的放大漸變效果”">&#8203;</a></h1>
<p>做法很簡單，就是做兩個 <code>d3.svg.arc()</code> 然後在 <code>mouseover</code> &amp; <code>mouseout</code> 的時候改變 <code>d</code> 屬性即可。</p>
<p>這裏就拿 <a href="http://bl.ocks.org/mbostock/3887193" target="_blank" rel="noreferrer">http://bl.ocks.org/mbostock/3887193</a> 作為範例。</p>
<p>DEMO: (original charts)
<a href="http://output.jsbin.com/darexo/1/" target="_blank" rel="noreferrer">http://output.jsbin.com/darexo/1/</a></p>
<p>DEMO2: (add hover effect)</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="snippet-d3-js-甜甜圈圖-donut-chart-的放大漸變效果" tabindex="-1">snippet D3-js 甜甜圈圖 (donut chart) 的放大漸變效果 <a class="header-anchor" href="#snippet-d3-js-甜甜圈圖-donut-chart-的放大漸變效果" aria-label="Permalink to “snippet D3-js 甜甜圈圖 (donut chart) 的放大漸變效果”">&#8203;</a></h1>
<p>做法很簡單，就是做兩個 <code>d3.svg.arc()</code> 然後在 <code>mouseover</code> &amp; <code>mouseout</code> 的時候改變 <code>d</code> 屬性即可。</p>
<p>這裏就拿 <a href="http://bl.ocks.org/mbostock/3887193" target="_blank" rel="noreferrer">http://bl.ocks.org/mbostock/3887193</a> 作為範例。</p>
<p>DEMO: (original charts)
<a href="http://output.jsbin.com/darexo/1/" target="_blank" rel="noreferrer">http://output.jsbin.com/darexo/1/</a></p>
<p>DEMO2: (add hover effect)</p>
<hr>
<p><a href="http://output.jsbin.com/darexo/3/" target="_blank" rel="noreferrer">http://output.jsbin.com/darexo/3/</a></p>
<iframe class="jsbin-embed" src="http://jsbin.com/darexo/3/embed?output" frameborder="0" height="400"></iframe>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// original</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> arc </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.svg.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">arc</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">outerRadius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(radius </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">innerRadius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(radius </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 70</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// arc with scaling</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> arcOver </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> d3.svg.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">arc</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">outerRadius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(radius)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">innerRadius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(radius </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 70</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><p>中間略</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// adding mouse events with transition</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">g.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">selectAll</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">".arc > path"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">	.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">on</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">		"mouseover"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">i</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">			d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">transition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">duration</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">250</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'d'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, arcOver);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">		"mouseout"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">i</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">			d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">transition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">duration</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">250</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'d'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, arc);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><p>補充：感謝小馬提出 hover 在 text 上會無法執行動畫的問題。
只要把 mouse event 改綁在上一層的 <code>&lt;g&gt;</code> 上面，或是在 text 的 CSS 下 <code>pointer-events: none;</code> 取消它的 pointer 事件即可。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  g.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">on</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "mouseover"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">i</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.childNodes[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">transition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">duration</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">250</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'d'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, arcOver);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "mouseout"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">i</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.childNodes[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">transition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">().</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">duration</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">250</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'d'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, arc);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div>]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>D3-js</category>
            <category>donut chart</category>
        </item>
        <item>
            <title><![CDATA[淺談 Google map Heat map API]]></title>
            <link>https://kurohsu.dev/notes/a-brief-talk-on-google-map-heat-map-api.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/a-brief-talk-on-google-map-heat-map-api.html</guid>
            <pubDate>Wed, 29 Apr 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="淺談-google-map-heat-map-api" tabindex="-1">淺談 Google map Heat map API <a class="header-anchor" href="#淺談-google-map-heat-map-api" aria-label="Permalink to “淺談 Google map Heat map API”">&#8203;</a></h1>
<p>前陣子因為工作的需要，稍微研究了一下 Google Map 提供的 HeatMap (熱點圖/熱圖) API，實作方面也滿容易的，在這裡就簡單做個紀錄。</p>
<p><strong>HeatMap</strong> (以下稱熱圖) 是用來表示<strong>資料數值強度與位置的可視化</strong>，Google 熱圖 API 在預設情況下，高強度的數值會以紅色表示，低強度的數值則是以綠色來表示。</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/262458/gg6DtVUASmSRqxaw8gpk_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-04-29%20%E4%B8%8B%E5%8D%883.37.46.png" alt="螢幕快照 2015-04-29 下午3.37.46.png">
圖片來源: <a href="https://developers.google.com/maps/documentation/javascript/examples/layer-heatmap" target="_blank" rel="noreferrer">https://developers.google.com/maps/documentation/javascript/examples/layer-heatmap</a></p>
<p>有了簡單的認知後，那麼就來說明如何透過 Google Map API 在 Google Map 載入熱圖。</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="淺談-google-map-heat-map-api" tabindex="-1">淺談 Google map Heat map API <a class="header-anchor" href="#淺談-google-map-heat-map-api" aria-label="Permalink to “淺談 Google map Heat map API”">&#8203;</a></h1>
<p>前陣子因為工作的需要，稍微研究了一下 Google Map 提供的 HeatMap (熱點圖/熱圖) API，實作方面也滿容易的，在這裡就簡單做個紀錄。</p>
<p><strong>HeatMap</strong> (以下稱熱圖) 是用來表示<strong>資料數值強度與位置的可視化</strong>，Google 熱圖 API 在預設情況下，高強度的數值會以紅色表示，低強度的數值則是以綠色來表示。</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/262458/gg6DtVUASmSRqxaw8gpk_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-04-29%20%E4%B8%8B%E5%8D%883.37.46.png" alt="螢幕快照 2015-04-29 下午3.37.46.png">
圖片來源: <a href="https://developers.google.com/maps/documentation/javascript/examples/layer-heatmap" target="_blank" rel="noreferrer">https://developers.google.com/maps/documentation/javascript/examples/layer-heatmap</a></p>
<p>有了簡單的認知後，那麼就來說明如何透過 Google Map API 在 Google Map 載入熱圖。</p>
<hr>
<p>首先是在載入 Google Maps API 時，必須加入 <strong>visualization</strong> 這個 library。在後面加入 <code>libraries=visualization</code> 即可，要是想再加入其他 library 可用 <code>,</code> 逗號隔開。如果沒有載入 visualization 這個 library，熱圖會無法顯示。</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"text/javascript"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"https://maps.googleapis.com/maps/api/js?libraries=visualization"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>再來，因爲熱圖其實是一連串 <strong>座標點</strong> 與 <strong>數值</strong> 的集合，所以我們要先準備好一個陣列來存放 LatLng 物件的集合：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 存放 google.maps.LatLng 物件的陣列</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> heatmapData </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LatLng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">37.782</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">122.447</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LatLng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">37.782</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">122.445</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">	// 以下略 ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 產生一個 Heatmap Layer</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> heatmap </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.visualization.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">HeatmapLayer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  data: heatmapData</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 將 heatmap 圖層加入至 map</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">heatmap.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(map);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br></div></div><p>如果想要改變熱圖樣式的話也很簡單，有兩種方式，第一種是在 <code>new google.maps.visualization.HeatmapLayer</code> 時加入樣式的設定，如：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> heatmap </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.visualization.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">HeatmapLayer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  data: heatmapData,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  gradient: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'transparent'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#f00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#0f0'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#00f'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],	</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 指定顏色範圍 ex:透明, 紅, 綠, 藍</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  radius: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,				</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 每個點的半徑 (單位 px)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  opacity: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.5</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">			// 熱圖圖層透明度 (0 ~ 1)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>或是想改變已載入熱圖的樣式，可以透過 <code>heatmap.set()</code> :</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 改變範圍顏色</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">heatmap.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">set</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'gradient'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'transparent'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#f00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#0f0'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#00f'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 改變透明度</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">heatmap.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">set</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'opacity'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>完整範例可以參考 Google Heat Map 文件：
<a href="https://developers.google.com/maps/documentation/javascript/examples/layer-heatmap" target="_blank" rel="noreferrer">https://developers.google.com/maps/documentation/javascript/examples/layer-heatmap</a></p>
<p>接著，同場加映。</p>
<p>如果今天我們有一份完整的 GeoJSON (關於 GeoJSON 可參考<a href="http://kuro.tw/posts/2015/04/28/through-the-google-maps-api-geojson-data" target="_blank" rel="noreferrer">前篇 GeoJSON 介紹</a>)文件，是否可以直接生成熱圖？ 當然沒問題。</p>
<p>我們只要將取得的 GeoJSON 解析出來，並存入陣列內就可以輕鬆地生成熱圖。</p>
<p>這是 GeoJSON 範例的格式，可以看出來這是一個 Point，而且有個屬性 valueCount，值為 5。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Feature"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  "geometry"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Point"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      "coordinates"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">121.52803907522677</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25.036051507818932</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  "properties"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"valueCount"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 畫熱圖</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> draw_heatmap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">results</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> heatmapData </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [];</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> results.features.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">length</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; i</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">++</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 取得座標</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> coords </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> results.features[i].geometry.coordinates;</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 要注意： LatLng 物件的經緯度順序與 GeoJSON 的座標順序相反</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> latLng </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LatLng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(coords[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], coords[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> weightedLoc </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 位置</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      location: latLng,</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">      // 單位強度，這裏由 GeoJSON 內的 valueCount 屬性取得</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      weight: results.features[i].properties.valueCount</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    heatmapData.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">push</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(weightedLoc);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 生成熱圖圖層</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> heatmap </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.visualization.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">HeatmapLayer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    data: heatmapData,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    dissipating: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    map: map,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    radius: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">40</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    gradient: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'transparent'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#f00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#0f0'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'#00f'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br></div></div><p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/262458/jE7EkvSumXUFaRKArSMw_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-04-29%20%E4%B8%8B%E5%8D%883.50.00.png" alt="螢幕快照 2015-04-29 下午3.50.00.png">
DEMO: <a href="http://jsbin.com/xafagi/1/" target="_blank" rel="noreferrer">http://jsbin.com/xafagi/1/</a></p>
<p>如果想要 debug 確認生成的熱圖是否正確，因為我們是利用 GeoJSON 產生熱圖，所以我們可以透過 <code>map.data.addGeoJson( geoJson );</code> 來加入 marker 供我們確認。
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/262458/iU5KQsFgRQKTwgEjNraG_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-04-29%20%E4%B8%8B%E5%8D%883.59.13.png" alt="螢幕快照 2015-04-29 下午3.59.13.png">
DEMO: <a href="http://jsbin.com/xafagi/2/" target="_blank" rel="noreferrer">http://jsbin.com/xafagi/2/</a></p>
<p>參考資料：
<a href="https://developers.google.com/maps/documentation/javascript/examples/layer-heatmap" target="_blank" rel="noreferrer">https://developers.google.com/maps/documentation/javascript/examples/layer-heatmap</a>
<a href="https://developers.google.com/maps/documentation/javascript/heatmaplayer" target="_blank" rel="noreferrer">https://developers.google.com/maps/documentation/javascript/heatmaplayer</a></p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>Google Map</category>
            <category>google map</category>
            <category>Heatmap</category>
        </item>
        <item>
            <title><![CDATA[透過 Google Maps API 處理 GeoJSON 資料]]></title>
            <link>https://kurohsu.dev/notes/through-the-google-maps-api-geojson-data.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/through-the-google-maps-api-geojson-data.html</guid>
            <pubDate>Tue, 28 Apr 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="透過-google-maps-api-處理-geojson-資料" tabindex="-1">透過 Google Maps API 處理 GeoJSON 資料 <a class="header-anchor" href="#透過-google-maps-api-處理-geojson-資料" aria-label="Permalink to “透過 Google Maps API 處理 GeoJSON 資料”">&#8203;</a></h1>
<p>在說明 Google Map 如何存取 GeoJSON 前，先來簡單介紹 GeoJSON。</p>
<p><a href="http://geojson.org/" title="GeoJSON" target="_blank" rel="noreferrer">GeoJSON</a> 是一種專門處理地理資訊 (GIS) 結構的 JSON 標準格式。 一個 GeoJSON 物件可以用來代表<strong>點</strong> (Point)，<strong>線</strong> (LineString)，<strong>多邊形</strong> (Polygon) 等等的幾何結構，以及<strong>特徵</strong> (Feature) 的集合，或是<strong>一系列的特徵</strong> (FeatureCollection)。</p>
<p>也因為 GeoJSON 是一種基於 JSON 的公開標準，其結構簡單且容易讀取的特性也廣受開發者的歡迎，有不少開發程式庫開始支援 GeoJSON 的處理，也有許多政府開放資料(官方或非官方)開始提供 GeoJSON 作為其資料格式。</p>
<p>一個簡單的 GeoJSON 會長得像這個樣子：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<h1 id="透過-google-maps-api-處理-geojson-資料" tabindex="-1">透過 Google Maps API 處理 GeoJSON 資料 <a class="header-anchor" href="#透過-google-maps-api-處理-geojson-資料" aria-label="Permalink to “透過 Google Maps API 處理 GeoJSON 資料”">&#8203;</a></h1>
<p>在說明 Google Map 如何存取 GeoJSON 前，先來簡單介紹 GeoJSON。</p>
<p><a href="http://geojson.org/" title="GeoJSON" target="_blank" rel="noreferrer">GeoJSON</a> 是一種專門處理地理資訊 (GIS) 結構的 JSON 標準格式。 一個 GeoJSON 物件可以用來代表<strong>點</strong> (Point)，<strong>線</strong> (LineString)，<strong>多邊形</strong> (Polygon) 等等的幾何結構，以及<strong>特徵</strong> (Feature) 的集合，或是<strong>一系列的特徵</strong> (FeatureCollection)。</p>
<p>也因為 GeoJSON 是一種基於 JSON 的公開標準，其結構簡單且容易讀取的特性也廣受開發者的歡迎，有不少開發程式庫開始支援 GeoJSON 的處理，也有許多政府開放資料(官方或非官方)開始提供 GeoJSON 作為其資料格式。</p>
<p>一個簡單的 GeoJSON 會長得像這個樣子：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"FeatureCollection"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "features"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [{</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Feature"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "geometry"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Point"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "coordinates"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">102.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "properties"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "prop0"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"value0"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }, {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Feature"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "geometry"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"LineString"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "coordinates"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">102.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">103.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">104.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">105.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "properties"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "prop0"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"value0"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "prop1"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.0</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }, {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Feature"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "geometry"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Polygon"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "coordinates"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                    [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">100.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                    [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">101.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                    [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">101.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                    [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">100.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                    [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">100.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0.0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">        "properties"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "prop0"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"value0"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            "prop1"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                "this"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"that"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br></div></div><p>如上所示，每一筆資料都會是一個「<strong>Feature</strong>」物件，地理位置相關資訊會存放在 geometry 物件內，其中分別有「<strong>type</strong>」以及「<strong>coordinates</strong>」屬性，type 用來表示資料類別，可以是點，線，甚至是多邊形等；而 coordinates 用來存放經緯度座標。而其他的相關資訊則會放在「<strong>properties</strong>」內，以 <strong>key: value</strong> 方式呈現。</p>
<p>簡單介紹就到此，有興趣的朋友可以參考 <a href="http://geojson.org/" target="_blank" rel="noreferrer">http://geojson.org/</a>
註：這裏有中國網友翻譯的 GeoJSON 規格 <a href="http://www.oschina.net/translate/geojson-spec" target="_blank" rel="noreferrer">http://www.oschina.net/translate/geojson-spec</a></p>
<p>回到主題。
Google Map 匯入 GeoJSON 的方式非常簡單，如果已經有一個完整的 GeoJSON 檔案的話，那麼透過</p>
<p><code>map.data.loadGeoJson(FILE-URL);</code></p>
<p>利用這一行程式碼就可以載入 GeoJSON 至 Google Map 的 Data Layer 了。</p>
<p>簡單範例：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> map;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> initMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 地圖初始設定</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> mapOptions </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        center: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LatLng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25.04674</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">121.54168</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        zoom: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">13</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        mapTypeId: google.maps.MapTypeId.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">ROADMAP</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // GeoJSON file</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> url </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'https://dl.dropboxusercontent.com/u/12537630/geo.json'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> mapElement </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getElementById</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"mapDiv"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // Google 地圖初始化</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    map </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(mapElement, mapOptions);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 載入 GeoJSON 資料</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    map.data.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">loadGeoJson</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(url);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br></div></div><p>DEMO: <a href="http://jsbin.com/totuzisobo/1/" target="_blank" rel="noreferrer">http://jsbin.com/totuzisobo/1/</a>
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/262352/sSKDGsS7RUqG2TluFYJC_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-04-28%20%E4%B8%8B%E5%8D%887.15.40.png" alt="螢幕快照 2015-04-28 下午7.15.40.png"></p>
<p>如果希望載入的資料可以有自訂的樣式的話，可以透過 <code>map.data.setStyle</code> 在載入前指定好希望的 style，這裏以 marker 的圖示為例：</p>
<p>圖示資訊我放在 GeoJSON 的 <code>properties.icon</code> 內，如下 (其中一筆 point 的內容)：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Feature"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  "geometry"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">	  "type"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Point"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">	  "coordinates"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: [ </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">121.51771545410156</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25.028294990979614</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  "properties"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  	"icon"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"http://google-maps-icons.googlecode.com/files/vegetarian.png"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>於是我們可以透過 <code>feature.getProperty('icon')</code> 去指定每一筆 point 的圖示：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 自訂樣式</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  map.data.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setStyle</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">feature</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'icon'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: feature.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getProperty</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'icon'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) };</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 載入 GeoJSON 資料</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  map.data.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">loadGeoJson</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(url);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br></div></div><p>DEMO: <a href="http://jsbin.com/xikaqe/1/" target="_blank" rel="noreferrer">http://jsbin.com/xikaqe/1/</a>
<img src="http://user-image.logdown.io/user/3292/blog/3340/post/262352/u5lfzGXOQVaVY7zkw57q_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-04-28%20%E4%B8%8B%E5%8D%887.15.46.png" alt="螢幕快照 2015-04-28 下午7.15.46.png"></p>
<p>如果希望透過 ajax 方式載入 GeoJSON 的話也非常簡單，在取得 ajax 回傳資料時，用 <code>map.data.addGeoJson(res);</code> 這行就可以載入資料了。</p>
<p>以 jQuery 為例，如:</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">FILE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">URL</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">res</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) { map.data.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">addGeoJson</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(res); });</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>有個整理好的 GeoJSON，只需要一行就可以很輕鬆地在 Google Map 產生想要的圖標了。
如果想在圖表上處理點擊等等的事件，還是得另外處理喔。</p>
<p>參考資料:
<a href="https://developers.google.com/maps/documentation/javascript/examples/layer-data-simple" target="_blank" rel="noreferrer">https://developers.google.com/maps/documentation/javascript/examples/layer-data-simple</a>
<a href="https://developers.google.com/maps/documentation/javascript/examples/layer-data-quakes" target="_blank" rel="noreferrer">https://developers.google.com/maps/documentation/javascript/examples/layer-data-quakes</a></p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>Google Map</category>
            <category>google map</category>
            <category>GeoJSON</category>
        </item>
        <item>
            <title><![CDATA[透過 Google map Geocoder API 以經緯度轉換地址資訊]]></title>
            <link>https://kurohsu.dev/notes/address-information-is-obtained-through-google-map-geocoder-with-the-latitude-and-longitude.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/address-information-is-obtained-through-google-map-geocoder-with-the-latitude-and-longitude.html</guid>
            <pubDate>Mon, 27 Apr 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="透過-google-map-geocoder-api-以經緯度轉換地址資訊" tabindex="-1">透過 Google map Geocoder API 以經緯度轉換地址資訊 <a class="header-anchor" href="#透過-google-map-geocoder-api-以經緯度轉換地址資訊" aria-label="Permalink to “透過 Google map Geocoder API 以經緯度轉換地址資訊”">&#8203;</a></h1>
<p>前陣子因為需求的關係，需要以經緯度來轉換地址，幸好 Google map API 有提供 Geocoder 可以轉換大略地址的服務。使用方式非常簡單，我們這裡以立法院的經緯度 (25.0439892, 121.5212213) 為例：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> geocoder </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Geocoder</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// google.maps.LatLng 物件</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> coord </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LatLng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25.0439892</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">121.5212213</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<h1 id="透過-google-map-geocoder-api-以經緯度轉換地址資訊" tabindex="-1">透過 Google map Geocoder API 以經緯度轉換地址資訊 <a class="header-anchor" href="#透過-google-map-geocoder-api-以經緯度轉換地址資訊" aria-label="Permalink to “透過 Google map Geocoder API 以經緯度轉換地址資訊”">&#8203;</a></h1>
<p>前陣子因為需求的關係，需要以經緯度來轉換地址，幸好 Google map API 有提供 Geocoder 可以轉換大略地址的服務。使用方式非常簡單，我們這裡以立法院的經緯度 (25.0439892, 121.5212213) 為例：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> geocoder </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Geocoder</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// google.maps.LatLng 物件</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> coord </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LatLng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25.0439892</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">121.5212213</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 傳入 latLng 資訊至 geocoder.geocode</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">geocoder.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">geocode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'latLng'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: coord }, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">results</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">status</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (status </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.GeocoderStatus.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">OK</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 如果有資料就會回傳</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (results) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(results[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 經緯度資訊錯誤</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    alert</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Reverse Geocoding failed because: "</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> status);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br></div></div><p>我們傳入經緯度資訊 google.maps.LatLng 物件，透過 <code>geocoder.geocode</code> 轉換出來的結果會是這樣的：</p>
<p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/262289/0bIqy71dT8Sb3YwERlEs_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-04-27%20%E4%B8%8B%E5%8D%8810.42.56.png" alt="螢幕快照 2015-04-27 下午10.42.56.png"></p>
<p>如上圖，Google map Geocoder API 會回傳一連串的地址資訊，如果你需要的是已經整合好格式的地址，那麼將上面範例中的<code>results[0]</code> 改為 <code>results[0].formatted_address</code> 就可以取得「<strong>100台灣台北市中正區青島東路1號</strong>」這樣的地址資訊了。</p>
<p>下面提供一個完整的範例供各位參考：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> map, geocoder, popup;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> initMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    geocoder </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Geocoder</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    popup </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">InfoWindow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 地圖初始設定</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> mapOptions </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        center: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">LatLng</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25.04674</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">121.52168</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        zoom: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">16</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        mapTypeId: google.maps.MapTypeId.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">ROADMAP</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> mapElement </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getElementById</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"mapDiv"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // Google 地圖初始化</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    map </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(mapElement, mapOptions);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 設定 Google map 繪圖控制項</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> drawingManager </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.drawing.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">DrawingManager</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        drawingMode: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        drawingControl: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        drawingControlOptions: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            position: google.maps.ControlPosition.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">TOP_CENTER</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            drawingModes: [google.maps.drawing.OverlayType.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">MARKER</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 開啟 Google map 繪圖控制項</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    drawingManager.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(map);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 在地圖中加入 marker</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    google.maps.event.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">addListener</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(drawingManager, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'markercomplete'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">marker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 取得 marker 的經緯座標</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> markerPosition </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> marker.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getPosition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 將經緯度透過 Google map Geocoder API 反查地址</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        geocoder.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">geocode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">          'latLng'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: markerPosition</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">results</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">status</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (status </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">===</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.GeocoderStatus.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">OK</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">                if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (results) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">                    // 將取得的資訊傳入 marker 訊息泡泡</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">                    showAddress</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(results[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], marker);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">                alert</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Reverse Geocoding failed because: "</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> status);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // 設定 marker 的訊息泡泡</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> showAddress</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">marker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        map.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setCenter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(marker.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getPosition</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">());</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // 顯示傳入的地址資訊</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> popupContent </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '&#x3C;b>地址: &#x3C;/b> '</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> +</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> result.formatted_address;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        popup.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setContent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(popupContent);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        popup.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">open</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(map, marker);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br><span class="line-number">55</span><br><span class="line-number">56</span><br><span class="line-number">57</span><br><span class="line-number">58</span><br><span class="line-number">59</span><br><span class="line-number">60</span><br></div></div><p><img src="http://user-image.logdown.io/user/3292/blog/3340/post/262289/boE1unBmQsCitdOxHG01_%E8%9E%A2%E5%B9%95%E5%BF%AB%E7%85%A7%202015-04-27%20%E4%B8%8B%E5%8D%8810.52.20.png" alt="螢幕快照 2015-04-27 下午10.52.20.png"></p>
<p>Demo: <a href="http://jsbin.com/penuqofabe/1/" target="_blank" rel="noreferrer">http://jsbin.com/penuqofabe/1/</a></p>
<p>參考網址: <a href="https://developers.google.com/maps/documentation/javascript/geocoding?hl=zh-tw" target="_blank" rel="noreferrer">Google Map 地理編碼服務</a></p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>Google Map</category>
            <category>google map</category>
            <category>Geocoder</category>
        </item>
        <item>
            <title><![CDATA[note jQuery 與 d3-js 的一些不同之處]]></title>
            <link>https://kurohsu.dev/notes/note-jquery-and-d3js-some-of-these-differences.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/note-jquery-and-d3js-some-of-these-differences.html</guid>
            <pubDate>Tue, 21 Apr 2015 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="note-jquery-與-d3-js-的一些不同之處" tabindex="-1">note jQuery 與 d3-js 的一些不同之處 <a class="header-anchor" href="#note-jquery-與-d3-js-的一些不同之處" aria-label="Permalink to “note jQuery 與 d3-js 的一些不同之處”">&#8203;</a></h1>
<p>以前跟人家介紹 d3-js 的時候，我都會笑稱 d3-js 其實就是 SVG 界的 jQuery。
但是最近發現不少人對兩者的 select 以及 append 有些疑問，特別把它寫下來紀錄。</p>
<p>先說大家比較熟悉的 jQuery：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  $</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"body"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"&#x3C;div>&#x3C;/div>"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<h1 id="note-jquery-與-d3-js-的一些不同之處" tabindex="-1">note jQuery 與 d3-js 的一些不同之處 <a class="header-anchor" href="#note-jquery-與-d3-js-的一些不同之處" aria-label="Permalink to “note jQuery 與 d3-js 的一些不同之處”">&#8203;</a></h1>
<p>以前跟人家介紹 d3-js 的時候，我都會笑稱 d3-js 其實就是 SVG 界的 jQuery。
但是最近發現不少人對兩者的 select 以及 append 有些疑問，特別把它寫下來紀錄。</p>
<p>先說大家比較熟悉的 jQuery：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  $</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"body"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"&#x3C;div>&#x3C;/div>"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"&#x3C;em>&#x3C;/em>"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div><p>上面這段程式碼產生的 HTML 應該會是這樣的</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">em</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">em</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p>而在 d3-js 的情況：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"body"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"div"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      .</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"em"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br></div></div><p>所產生的 HTML 結構會是這樣的：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">em</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">em</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>兩者最大的差異點在於 jQuery 的</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  $</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"body"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>回傳的會是 $('body') 本身，而 d3-js 的</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  d3.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"body"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>回傳的會是 append 後的 DOM
(以上面的範例來說，<code>d3.select(&quot;body&quot;).append('div')</code> 回傳的就會是 <code>&lt;div&gt;&lt;/div&gt;</code> )，
所以再次執行 append 時，就會將 DOM 加入在前一個 append 的元素，也就是 <code>&lt;div&gt;&lt;/div&gt;</code> 之內。</p>
<p>那麼 jQuery 是否也可以像 d3-js 這樣 append 在新 DOM 內呢？
先將 em 包在 div 內，最外層再包一層 append 就可以做到了。
像這樣：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  $</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"body"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x3C;div>'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">append</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">$</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'&#x3C;em>'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) ) );</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div>]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>D3-js</category>
            <category>jQuery</category>
            <category>d3-js</category>
        </item>
        <item>
            <title><![CDATA[偵測 Google Map 的 InfoWindow 訊息框是否被開啟 (v3)]]></title>
            <link>https://kurohsu.dev/notes/google-map-infowindow-bubble-detection-box-is-opened.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/google-map-infowindow-bubble-detection-box-is-opened.html</guid>
            <pubDate>Fri, 08 Aug 2014 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="偵測-google-map-的-infowindow-訊息框是否被開啟-v3" tabindex="-1">偵測 Google Map 的 InfoWindow 訊息框是否被開啟 (v3) <a class="header-anchor" href="#偵測-google-map-的-infowindow-訊息框是否被開啟-v3" aria-label="Permalink to “偵測 Google Map 的 InfoWindow 訊息框是否被開啟 (v3)”">&#8203;</a></h1>
<p>因為 Google Map 的 InfoWindow 是被新增出來的物件，所以我們可以透過修改 prototype 的方式，替 InfoWindow 增加新的 method。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  google.maps.InfoWindow.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">prototype</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">isOpen</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> map </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (map </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> typeof</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> map </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "undefined"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br></div></div>]]></description>
            <content:encoded><![CDATA[<h1 id="偵測-google-map-的-infowindow-訊息框是否被開啟-v3" tabindex="-1">偵測 Google Map 的 InfoWindow 訊息框是否被開啟 (v3) <a class="header-anchor" href="#偵測-google-map-的-infowindow-訊息框是否被開啟-v3" aria-label="Permalink to “偵測 Google Map 的 InfoWindow 訊息框是否被開啟 (v3)”">&#8203;</a></h1>
<p>因為 Google Map 的 InfoWindow 是被新增出來的物件，所以我們可以透過修改 prototype 的方式，替 InfoWindow 增加新的 method。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  google.maps.InfoWindow.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">prototype</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">isOpen</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> map </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getMap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (map </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> typeof</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> map </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "undefined"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">---</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> popup </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> google.maps.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">InfoWindow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  popup.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">isOpen</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();  </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// will return ture or false.</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><p>參考資料： <a href="http://stackoverflow.com/a/12410385" target="_blank" rel="noreferrer">http://stackoverflow.com/a/12410385</a></p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>Google Map</category>
            <category>google map</category>
            <category>infoWindow</category>
        </item>
        <item>
            <title><![CDATA[PHP 將 HTML5 Canvas 產生的圖片上傳至伺服器端處理]]></title>
            <link>https://kurohsu.dev/notes/php-html5-canvas-resulting-base64-datauri-images-will-be-uploaded-to-the-server-side-processing.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/php-html5-canvas-resulting-base64-datauri-images-will-be-uploaded-to-the-server-side-processing.html</guid>
            <pubDate>Sun, 08 Sep 2013 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="php-將-html5-canvas-產生的圖片上傳至伺服器端處理" tabindex="-1">PHP 將 HTML5 Canvas 產生的圖片上傳至伺服器端處理 <a class="header-anchor" href="#php-將-html5-canvas-產生的圖片上傳至伺服器端處理" aria-label="Permalink to “PHP 將 HTML5 Canvas 產生的圖片上傳至伺服器端處理”">&#8203;</a></h1>
]]></description>
            <content:encoded><![CDATA[<h1 id="php-將-html5-canvas-產生的圖片上傳至伺服器端處理" tabindex="-1">PHP 將 HTML5 Canvas 產生的圖片上傳至伺服器端處理 <a class="header-anchor" href="#php-將-html5-canvas-產生的圖片上傳至伺服器端處理" aria-label="Permalink to “PHP 將 HTML5 Canvas 產生的圖片上傳至伺服器端處理”">&#8203;</a></h1>
<hr>
<p>一般來說有兩種處理方式：</p>
<h2 id="直接把-datauri-字串儲存起來-等要用的時候直接輸出至-img-的-src-屬性或是-css-中" tabindex="-1">直接把 DataURI 字串儲存起來，等要用的時候直接輸出至 <code>&lt;img&gt;</code> 的 src 屬性或是 CSS 中： <a class="header-anchor" href="#直接把-datauri-字串儲存起來-等要用的時候直接輸出至-img-的-src-屬性或是-css-中" aria-label="Permalink to “直接把 DataURI 字串儲存起來，等要用的時候直接輸出至 &lt;img&gt; 的 src 屬性或是 CSS 中：”">&#8203;</a></h2>
<p>像這樣：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">img</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">&#x3C;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">?php echo $base64_img_string; ?>"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>實際執行的時候會長得像這樣：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">img</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"data:image/png;base64,......(後略)"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><ul>
<li>
<p>優點：
簡單易用，幾乎不需要做任何處理。
HTTP Request 變少，傳送一個大檔案會比連續傳送多個拆解的小檔更快速有效率，且節省頻寬。</p>
</li>
<li>
<p>缺點：
IE7 以前的版本不支援 DataURI 格式。 IE8 開始雖然有支援，但限制大小不可超過 32KB
圖檔大的時候產生的 DataURI String 大得驚人(比原本的圖檔還大約 1/3)
圖檔修改後必須要重新編碼，相對的嵌入 DataURI String 的網頁也都要跟著修正
因圖片跟網頁綁再一起，不利快取</p>
</li>
</ul>
<h2 id="將-base64-datauri-送到-php-端儲存成圖片後使用" tabindex="-1">將 base64 DataURI 送到 PHP 端儲存成圖片後使用： <a class="header-anchor" href="#將-base64-datauri-送到-php-端儲存成圖片後使用" aria-label="Permalink to “將 base64 DataURI 送到 PHP 端儲存成圖片後使用：”">&#8203;</a></h2>
<p>如標題，應該不需要多加解釋了，就是利用 <code>base64_decode()</code> 將 data uri 反解，
直接看 code。 PHP 的部分：</p>
<div class="language-php line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;?</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">php</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 設定圖檔上傳路徑</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  define</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'UPLOAD_PATH'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'images/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 接收 POST 進來的 base64 DtatURI String</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $img </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $_POST[</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'data'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // 轉檔 &#x26; 存檔</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $img </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> str_replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'data:image/png;base64,'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">''</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, $img);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $img </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> str_replace</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">' '</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'+'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, $img);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $data </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> base64_decode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($img);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $file </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> UPLOAD_PATH</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> .</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> uniqid</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">.</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '.png'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $success </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> file_put_contents</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">($file, $data);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // output string</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  $output </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ($success) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '&#x3C;img src="'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $file </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">.</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'" alt="Canvas Image" />'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> :</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '&#x3C;p>Unable to save the file.&#x3C;/p>'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br></div></div><p>View 的部分 (HTML)：</p>
<div class="language-html line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;!</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">DOCTYPE</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> HTML</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">head</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">meta</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> charset</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"UTF-8"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>PHP base64 image decode demo&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">head</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    &#x3C;!-- 成功存檔的話會秀出 img 標籤以及圖檔，失敗的話會出現 Unable to save the file 的訊息 --></span></span>
<span class="line"><span style="--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">    &#x3C;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">?php print $output; ?></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br></div></div><p>這樣就完成了。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>HTML</category>
            <category>html</category>
            <category>html5</category>
            <category>canvas</category>
            <category>php</category>
        </item>
        <item>
            <title><![CDATA[淺談 ECMAScript 5 嚴格模式 (Strict Mode)]]></title>
            <link>https://kurohsu.dev/notes/ecmascript-5-strict-mode.html</link>
            <guid isPermaLink="false">https://kurohsu.dev/notes/ecmascript-5-strict-mode.html</guid>
            <pubDate>Sun, 27 Nov 2011 00:00:00 GMT</pubDate>
            <description><![CDATA[<h1 id="淺談-ecmascript-5-嚴格模式-strict-mode" tabindex="-1">淺談 ECMAScript 5 嚴格模式 (Strict Mode) <a class="header-anchor" href="#淺談-ecmascript-5-嚴格模式-strict-mode" aria-label="Permalink to “淺談 ECMAScript 5 嚴格模式 (Strict Mode)”">&#8203;</a></h1>
<p>自 ECMAScript 5 開始，增加了一個 嚴格模式 (Strict Mode) 的新特性。</p>
<p>ECMAScript 5 雖然可以跟前一版的 ECMAScript 3 相容 (ECMAScript 4 已廢棄)，但是，當我們宣告為 &quot;Strict Mode&quot; 後，那些 ECMAScript 5 不再建議使用的 ECMAScript 3 的舊語法會被全面禁止。
一旦出現，便會出現錯誤或拋出異常 (Exception)。</p>
<p>Strict Mode 的宣告方式有兩種：</p>
<p>若要在全域範圍內宣告使用 Strict Mode，只需在程式碼的第一行加上下面敘述，如：</p>
]]></description>
            <content:encoded><![CDATA[<h1 id="淺談-ecmascript-5-嚴格模式-strict-mode" tabindex="-1">淺談 ECMAScript 5 嚴格模式 (Strict Mode) <a class="header-anchor" href="#淺談-ecmascript-5-嚴格模式-strict-mode" aria-label="Permalink to “淺談 ECMAScript 5 嚴格模式 (Strict Mode)”">&#8203;</a></h1>
<p>自 ECMAScript 5 開始，增加了一個 嚴格模式 (Strict Mode) 的新特性。</p>
<p>ECMAScript 5 雖然可以跟前一版的 ECMAScript 3 相容 (ECMAScript 4 已廢棄)，但是，當我們宣告為 &quot;Strict Mode&quot; 後，那些 ECMAScript 5 不再建議使用的 ECMAScript 3 的舊語法會被全面禁止。
一旦出現，便會出現錯誤或拋出異常 (Exception)。</p>
<p>Strict Mode 的宣告方式有兩種：</p>
<p>若要在全域範圍內宣告使用 Strict Mode，只需在程式碼的第一行加上下面敘述，如：</p>
<hr>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"use strict"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>也可以在指定的函數內宣告使用 Strict Mode，在函數的第一行加上下面敘述，如：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Non-strict code...</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> func_UseStrictMode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  "use strict"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   // ... your code ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>因為宣告使用 Strict Mode 時只需要加入一段 &quot;use strict&quot;; 敘述式，所以不會對些舊時的瀏覽器造成任何相容性的問題。 再來簡單介紹一下 Strict Mode 與非 Strict Mode 的差異：</p>
<p><strong>變數：</strong>
JavaScript 使用變數的時候不一定要先宣告 (若直接使用未宣告的變數會自動變成全域變數，強烈不建議!) ，但在 Strict Mode 下，變數使用前必須要先用 var 宣告後才能拿來用，否則會出現錯誤。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"use strict"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (err) {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  alert</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(err);    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// throw exception !</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div><p>另外，在 Strict Mode 下刪除全域變數、函數，或是函數內的參數都會被認為是錯誤的語法。
如下：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"use strict"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> myfunc</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () { };</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">delete</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> i;         </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Error !</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">delete</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> myfunc;    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Error !</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> myfunc2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">arg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    delete</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> arg;    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Error !</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><p><strong>屬性</strong>：
在定義物件的屬性時，屬性名稱不可重複，同一個物件內不能重複定義相同屬性，否則會出現異常：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"use strict"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  foo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  foo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;     </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Error</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p><strong>函數與參數</strong>：
在 Strict Mode 下，函數的參數 (arguments) 不能有相同名稱的變數，如下：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> func1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">arg1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">arg2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) { }    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// OK</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> func2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">arg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">arg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) { }      </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Error</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p>同時，函數的 arguments 屬性在 Strict Mode 下也是唯讀的：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"use strict"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> func</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">arg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  arguments</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"..."</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">];    </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// not allow, Error.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><p>另外，對 arguments.caller 和 arguments.callee 的存取會出現錯誤。</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"use strict"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> test</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">	function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> inner</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  	// Don't exist, either</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    test.arguments </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;		</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Error</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    inner.caller </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;			</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Error</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br></div></div><p>因此，任何需要用到的匿名函數都必須先命名，例如：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">setTimeout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> later</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">	// do stuff...</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">	setTimeout</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( later, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> );</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br></div></div><p>最後，當使用 null 或者 undefined 作為 Function.prototype.call 或 Function.prototype.apply 方法的第一個參數時，函數內部的 this 將會指向 global object。</p>
<p>而 Strict mode 將會阻止其執行並拋出異常：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){ </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">call</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">null</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> );		</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Exception</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br></div></div><p><strong>eval</strong>：
eval 是保留的關鍵字，不能作為變數名、函數名、物件的屬性，甚至是變數都不行。
所以，以下的程式碼<strong>全都是錯誤</strong>的：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// All generate errors...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">obj.eval </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">obj.foo </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> eval;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> eval </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> eval </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">in</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ) {}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> eval</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(){}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> test</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">eval</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">eval</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">){}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Function</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"eval"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><p>另外，在 Strict mode 透過 eval 引入的變數也會無效，如下：</p>
<div class="language-javascript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">javascript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">eval</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"var a = false;"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">print</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">( </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">typeof</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a );			</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// undefined.</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br></div></div><p><strong>with() { }</strong>：
在 Strict mode 下沒有這個東西了，如果使用的話會被認為語法錯誤。</p>
<p><strong>其他</strong>：
在 Strict mode 下，不再支援 8 進位數字。
實際在 Firefox 8 測試的結果，會出現 &quot;octal literals and octal escape sequences are deprecated&quot; 的錯誤：在非 Strict mode 下，010 則是會 alert 出 8 這個數字。</p>
]]></content:encoded>
            <author>Kuro Hsu</author>
            <category>ECMAScript</category>
            <category>javascript</category>
            <category>ecmascript</category>
        </item>
    </channel>
</rss>