<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://ignikah-dev.github.io/blog</id>
    <title>Ignikah Log</title>
    <updated>2026-04-04T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://ignikah-dev.github.io/blog"/>
    <subtitle>Daily engineering notes from Ignikah Technology</subtitle>
    <icon>https://ignikah-dev.github.io/assets/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[外掛大腦：讓 AI 成為你的第二大腦，而不是替你思考]]></title>
        <id>https://ignikah-dev.github.io/blog/obsidian-ai-second-brain</id>
        <link href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain"/>
        <updated>2026-04-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[介紹一個結合 Obsidian 與 AI 的知識管理系統，讓 AI 幫你整理筆記、學習你的偏好，同時確保你始終是自己知識的主人。]]></summary>
        <content type="html"><![CDATA[<p>每次開新對話，AI 都不記得你是誰。你要重複解釋背景、重複說明偏好，就像每次看診都要從頭填病歷。</p>
<p>「外掛大腦」是一個解決這個問題的系統：讓 AI 能讀取你的知識庫，記住你是誰、在做什麼、喜歡什麼。</p>
<p>:::tip 立即下載
<strong><a href="https://github.com/rhincodon-studio/obsidian-brain-template" target="_blank" rel="noopener noreferrer" class="">GitHub: rhincodon-studio/obsidian-brain-template</a></strong></p>
<p>點擊 <strong>Use this template</strong> → 建立你的私人知識庫
:::</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="設計哲學ai-是電動牙刷不是毒品">設計哲學：AI 是電動牙刷，不是毒品<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#%E8%A8%AD%E8%A8%88%E5%93%B2%E5%AD%B8ai-%E6%98%AF%E9%9B%BB%E5%8B%95%E7%89%99%E5%88%B7%E4%B8%8D%E6%98%AF%E6%AF%92%E5%93%81" class="hash-link" aria-label="設計哲學：AI 是電動牙刷，不是毒品的直接連結" title="設計哲學：AI 是電動牙刷，不是毒品的直接連結" translate="no">​</a></h2>
<p>電動牙刷讓你刷牙刷得更乾淨，但你還是要自己刷。它不會替你思考，只是放大你的能力。</p>
<p>這個系統的設計目標：</p>
<table><thead><tr><th>電動牙刷</th><th>毒品</th></tr></thead><tbody><tr><td>你的邏輯，AI 執行</td><td>AI 替你決定</td></tr><tr><td>用越久，你越清楚自己</td><td>用越久，越依賴</td></tr><tr><td>拔掉 AI，知識還在</td><td>停用就崩潰</td></tr></tbody></table>
<blockquote>
<p>如果你發現自己「沒有 AI 就不知道怎麼整理筆記」，這個系統就失敗了。</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="核心功能">核心功能<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#%E6%A0%B8%E5%BF%83%E5%8A%9F%E8%83%BD" class="hash-link" aria-label="核心功能的直接連結" title="核心功能的直接連結" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="弱結構強智能">弱結構，強智能<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#%E5%BC%B1%E7%B5%90%E6%A7%8B%E5%BC%B7%E6%99%BA%E8%83%BD" class="hash-link" aria-label="弱結構，強智能的直接連結" title="弱結構，強智能的直接連結" translate="no">​</a></h3>
<p>不用記複雜的分類規則。丟進 Inbox，執行 <code>/intake</code>，AI 會幫你分類。</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">你負責：隨手記錄</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">AI 負責：分析內容 → 判斷歸屬 → 自動分類</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="自我進化但你是審核者">自我進化，但你是審核者<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#%E8%87%AA%E6%88%91%E9%80%B2%E5%8C%96%E4%BD%86%E4%BD%A0%E6%98%AF%E5%AF%A9%E6%A0%B8%E8%80%85" class="hash-link" aria-label="自我進化，但你是審核者的直接連結" title="自我進化，但你是審核者的直接連結" translate="no">​</a></h3>
<p>AI 會靜默學習你的偏好，但<strong>你確認後才生效</strong>。</p>
<p>Identity 相關的變更（使命、目標、信念）一定需要審批。AI 無法在你不知情的情況下改變你的「北極星」。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-個核心技能">6 個核心技能<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#6-%E5%80%8B%E6%A0%B8%E5%BF%83%E6%8A%80%E8%83%BD" class="hash-link" aria-label="6 個核心技能的直接連結" title="6 個核心技能的直接連結" translate="no">​</a></h3>
<table><thead><tr><th>指令</th><th>作用</th></tr></thead><tbody><tr><td><code>/journal</code></td><td>建立今日工作日誌</td></tr><tr><td><code>/intake</code></td><td>整理 Inbox，自動分類</td></tr><tr><td><code>/reflect</code></td><td>週度覆盤，聚合洞察</td></tr><tr><td><code>/maintain</code></td><td>健康檢查</td></tr><tr><td><code>/digest</code></td><td>處理 AI 觀察到的偏好</td></tr><tr><td><code>/writing</code></td><td>載入你的寫作風格</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="適合誰">適合誰？<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#%E9%81%A9%E5%90%88%E8%AA%B0" class="hash-link" aria-label="適合誰？的直接連結" title="適合誰？的直接連結" translate="no">​</a></h2>
<ul>
<li class="">筆記很亂，不知道怎麼整理</li>
<li class="">想讓 AI 記住自己的偏好</li>
<li class="">學生：每天記學習進度，累積成可複用筆記</li>
<li class="">工作者：讓 AI 協助管理項目和知識</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="下載與安裝">下載與安裝<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#%E4%B8%8B%E8%BC%89%E8%88%87%E5%AE%89%E8%A3%9D" class="hash-link" aria-label="下載與安裝的直接連結" title="下載與安裝的直接連結" translate="no">​</a></h2>
<div class="theme-tabs-container tabs-container tabList__CuJ"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_LNqP tabs__item--active">GitHub 網頁</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">命令行</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_Ymn6"><ol>
<li class="">前往 <strong><a href="https://github.com/rhincodon-studio/obsidian-brain-template" target="_blank" rel="noopener noreferrer" class="">rhincodon-studio/obsidian-brain-template</a></strong></li>
<li class="">點擊綠色按鈕 <strong>Use this template</strong> → <strong>Create a new repository</strong></li>
<li class="">命名為 <code>my-brain</code>，設為 <strong>Private</strong>（重要：你的日誌會在這裡）</li>
<li class="">Clone 到本地，用 Obsidian 打開</li>
</ol></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"># 建立你的私人 repo</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">gh repo create my-brain \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --template rhincodon-studio/obsidian-brain-template \</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --private</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># Clone 到本地</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">gh repo clone my-brain</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># 用 Obsidian 打開</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">cd my-brain &amp;&amp; open .</span><br></div></code></pre></div></div></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="新手入門指南">新手入門指南<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#%E6%96%B0%E6%89%8B%E5%85%A5%E9%96%80%E6%8C%87%E5%8D%97" class="hash-link" aria-label="新手入門指南的直接連結" title="新手入門指南的直接連結" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-建立你的知識庫">Step 1: 建立你的知識庫<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#step-1-%E5%BB%BA%E7%AB%8B%E4%BD%A0%E7%9A%84%E7%9F%A5%E8%AD%98%E5%BA%AB" class="hash-link" aria-label="Step 1: 建立你的知識庫的直接連結" title="Step 1: 建立你的知識庫的直接連結" translate="no">​</a></h3>
<ol>
<li class="">前往 <a href="https://github.com/rhincodon-studio/obsidian-brain-template" target="_blank" rel="noopener noreferrer" class="">obsidian-brain-template</a></li>
<li class="">點 <strong>Use this template</strong> → <strong>Create a new repository</strong></li>
<li class="">名稱填 <code>my-brain</code>，選 <strong>Private</strong></li>
<li class="">Clone 到電腦：<code>git clone https://github.com/你的帳號/my-brain.git</code></li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-安裝必要工具">Step 2: 安裝必要工具<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#step-2-%E5%AE%89%E8%A3%9D%E5%BF%85%E8%A6%81%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="Step 2: 安裝必要工具的直接連結" title="Step 2: 安裝必要工具的直接連結" translate="no">​</a></h3>
<ul>
<li class=""><strong><a href="https://obsidian.md/" target="_blank" rel="noopener noreferrer" class="">Obsidian</a></strong> — 免費的 Markdown 編輯器</li>
<li class=""><strong><a href="https://claude.ai/download" target="_blank" rel="noopener noreferrer" class="">Claude Code</a></strong> — AI 助理（需要 API key）</li>
</ul>
<p>用 Obsidian 打開 <code>my-brain</code> 資料夾，就能看到整個知識庫結構。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3-讓-ai-認識你">Step 3: 讓 AI 認識你<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#step-3-%E8%AE%93-ai-%E8%AA%8D%E8%AD%98%E4%BD%A0" class="hash-link" aria-label="Step 3: 讓 AI 認識你的直接連結" title="Step 3: 讓 AI 認識你的直接連結" translate="no">​</a></h3>
<p>打開 <code>Identity/TELOS.md</code>，填寫你的使命和目標：</p>
<div class="language-markdown codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-markdown codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token title important punctuation" style="color:#393A34">##</span><span class="token title important"> 使命</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token list punctuation" style="color:#393A34">-</span><span class="token plain"> M0: 成為能夠創造價值的軟體工程師</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token title important punctuation" style="color:#393A34">##</span><span class="token title important"> 目標</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token list punctuation" style="color:#393A34">-</span><span class="token plain"> G0: 2024 年完成 3 個 Side Project</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token title important punctuation" style="color:#393A34">##</span><span class="token title important"> 核心信念</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token list punctuation" style="color:#393A34">-</span><span class="token plain"> B0: 簡單比複雜好</span><br></div></code></pre></div></div>
<p>再打開 <code>Identity/CONTEXT.md</code>，寫下你現在在做什麼。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4-開始使用">Step 4: 開始使用<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#step-4-%E9%96%8B%E5%A7%8B%E4%BD%BF%E7%94%A8" class="hash-link" aria-label="Step 4: 開始使用的直接連結" title="Step 4: 開始使用的直接連結" translate="no">​</a></h3>
<p>在 <code>my-brain</code> 目錄啟動 Claude Code：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">cd my-brain</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">claude</span><br></div></code></pre></div></div>
<p>AI 會自動讀取你的身份設定。試試這些指令：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">/journal          # 開始今天的日誌</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">/intake           # 整理 Inbox（如果有東西的話）</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">/maintain         # 看看知識庫健康狀態</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5-日常使用">Step 5: 日常使用<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#step-5-%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8" class="hash-link" aria-label="Step 5: 日常使用的直接連結" title="Step 5: 日常使用的直接連結" translate="no">​</a></h3>
<p><strong>每天</strong>：想到什麼就丟進 <code>Inbox/</code> 或 <code>journals/</code></p>
<p><strong>有空時</strong>：執行 <code>/intake</code> 讓 AI 幫你分類</p>
<p><strong>每週</strong>：執行 <code>/reflect</code> 做週度覆盤</p>
<p>就這樣。不用記規則，AI 會學習你的習慣</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="模板包含什麼">模板包含什麼？<a href="https://ignikah-dev.github.io/blog/obsidian-ai-second-brain#%E6%A8%A1%E6%9D%BF%E5%8C%85%E5%90%AB%E4%BB%80%E9%BA%BC" class="hash-link" aria-label="模板包含什麼？的直接連結" title="模板包含什麼？的直接連結" translate="no">​</a></h2>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">├── Inbox/          # 收集箱</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Projects/       # 有截止日期的項目</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Areas/          # 持續維護的領域</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Resources/      # 可複用的知識</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Archives/       # 已完成的歸檔</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Identity/       # 你是誰（AI 讀取）</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── System/         # 系統設定</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">└── journals/       # 每日日誌</span><br></div></code></pre></div></div>
<p>加上：</p>
<ul>
<li class="">完整的 AI 提示詞（<code>.claude/CLAUDE.md</code>）</li>
<li class="">6 個預設技能</li>
<li class="">範例檔案和詳細說明</li>
</ul>
<hr>
<p>:::info 開始使用
<strong><a href="https://github.com/rhincodon-studio/obsidian-brain-template" target="_blank" rel="noopener noreferrer" class="">GitHub: rhincodon-studio/obsidian-brain-template</a></strong></p>
<p>亂的筆記比沒有筆記好。讓 AI 負責整理，你只需要負責輸入。
:::</p>]]></content>
        <author>
            <name>Founder</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="Obsidian" term="Obsidian"/>
        <category label="知識管理" term="知識管理"/>
        <category label="生產力" term="生產力"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Diffoci：讓容器建構可重現的驗證捷徑]]></title>
        <id>https://ignikah-dev.github.io/blog/diffoci-containers</id>
        <link href="https://ignikah-dev.github.io/blog/diffoci-containers"/>
        <updated>2025-12-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[簡介 Diffoci 如何把容器元件解構成可比對的片段，協助開發者在 CI 或本地重建出相同的映像。]]></summary>
        <content type="html"><![CDATA[<p>Diffoci 來自 <a href="https://github.com/reproducible-containers/diffoci" target="_blank" rel="noopener noreferrer" class="">reproducible-containers/diffoci</a>，它把容器的每一層、清單和設定拆解成容易比較的片段，用以檢查本地建構或 CI 執行是否真的可重複輸出同樣的映像。這篇分享 Diffoci 的角色、驗證流程與 CI 整合思路，讓你不必再用人工 diff 無數個 tarball。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="何謂-diffoci">何謂 Diffoci<a href="https://ignikah-dev.github.io/blog/diffoci-containers#%E4%BD%95%E8%AC%82-diffoci" class="hash-link" aria-label="何謂 Diffoci的直接連結" title="何謂 Diffoci的直接連結" translate="no">​</a></h2>
<p>Diffoci 的核心概念是「解構」容器映像。它會把 OCI image layout 和映像內部的檔案系統逐一展開成 tarball，再比對 metadata（manifest、config）與物理內容，這樣就能照顧到檔案權限、時間戳與層順序等細節，而不是只比較映像 ID。</p>
<p>比較時，Diffoci 會提供可經驗證的差異報表（包括檔案新增/刪除、二進位內容差異、metadata 不一致），也能把結果存成檔案以供追查。若你在本地手動 build 出來的映像與上游 CI 給的 artifact 有差異，Diffoci 能指出是哪一層、哪個檔案不對，甚至反推是否是建構參數或 buildkit cache 導致。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="驗證流程切片">驗證流程切片<a href="https://ignikah-dev.github.io/blog/diffoci-containers#%E9%A9%97%E8%AD%89%E6%B5%81%E7%A8%8B%E5%88%87%E7%89%87" class="hash-link" aria-label="驗證流程切片的直接連結" title="驗證流程切片的直接連結" translate="no">​</a></h2>
<p>Diffoci 支援把驗證拆成幾個步驟，方便串到工作流程：</p>
<ul>
<li class=""><strong>拉出參考映像</strong>：先把前次可信任的映像或某個 release 下載成 OCI layout（例如 <code>docker save</code> + <code>umoci unpack</code>），作為 baseline。</li>
<li class=""><strong>產生待驗證映像</strong>：用相同 Dockerfile/BuildKit pipeline 產出新的映像，並將它也轉成 OCI layout。</li>
<li class=""><strong>執行 Diffoci 比對</strong>：用 <code>diffoci diff</code>（或類似指令）比較兩個 OCI layout，差異會分門別類地列出檔案層、metadata 層與 config 層的差異。</li>
<li class=""><strong>分析報告</strong>：Diffoci 可輸出 XML/JSON 或 human-readable 形式的報告，並載入到 CI artifact，以便快速定位檔案不一致、層內重複或時間戳漂移等問題。</li>
<li class=""><strong>回饋開發流程</strong>：若差異在不可接受範圍（例如確保 byte-for-byte 相同），可以直接使該步驟 fail；若只是時間戳變動，可透過指令清除 metadata，確認誤判再走下一輪。</li>
</ul>
<p>這種分層比較讓你不必猜測哪個檔案被改寫，也避免單純比較 digest 時疏漏時間戳或權限的變化。此外，這個流程也可套用在多平台建構（例如 amd64 與 arm64），只要把不同平台的映像各自解構，就能逐層對齊差異。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="把-diffoci-拉進-ci">把 Diffoci 拉進 CI<a href="https://ignikah-dev.github.io/blog/diffoci-containers#%E6%8A%8A-diffoci-%E6%8B%89%E9%80%B2-ci" class="hash-link" aria-label="把 Diffoci 拉進 CI的直接連結" title="把 Diffoci 拉進 CI的直接連結" translate="no">​</a></h2>
<p>在 CI（比如 GitHub Actions、GitLab CI）中，只要把 Diffoci 當成一個步驟來執行，就能在建構完成後立刻驗證結果：</p>
<ol>
<li class="">先執行 build，產生映像與 OCI layout。</li>
<li class="">從 release 或 artifacts 下載先前的 OCI layout 作 baseline。</li>
<li class="">以 Diffoci 比對兩者，Diffoci 會回傳差異數據與檔案清單。</li>
<li class="">根據 policy 決定：若要 strict reproducibility，就讓差異導致 job fail；若只需資料一致，就把報告存起來讓人 review。</li>
</ol>
<p>透過 Diffoci 的報告還可以自動化產生監控告警，例如「新映像多出了私密金鑰」或「某系統套件版本被升級」，以便在 release 前即時攔截潛在風險。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="小結">小結<a href="https://ignikah-dev.github.io/blog/diffoci-containers#%E5%B0%8F%E7%B5%90" class="hash-link" aria-label="小結的直接連結" title="小結的直接連結" translate="no">​</a></h2>
<p>Diffoci 並非單純把映像 tarball 做 <code>diff</code>，而是把 OCI 元素拆解成會被 CI 消費的維度（metadata、層、檔案內容），再提供可讀報告與 exit code，讓可重現性從「理想」變成「pipeline guard」。若你的團隊在意每次 build 的 bitwise 相同性，或希望在多個環境比對結果，Diffoci 是一個值得納入的檢查步驟。</p>]]></content>
        <author>
            <name>Founder</name>
        </author>
        <category label="容器" term="容器"/>
        <category label="可重現性" term="可重現性"/>
        <category label="CI" term="CI"/>
        <category label="diffoci" term="diffoci"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[短網址設計]]></title>
        <id>https://ignikah-dev.github.io/blog/short-url-design</id>
        <link href="https://ignikah-dev.github.io/blog/short-url-design"/>
        <updated>2025-12-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[探討短網址常見的 hash、id+base62 與 snowflake/uuidv7 方案，並比較它們在唯一性、可排序與分散式需求下的取捨。]]></summary>
        <content type="html"><![CDATA[<p>短網址的核心目標，是用有限字元表達最多的唯一值。本篇釐清常見的三種生成思路，並談碰撞、資訊洩漏與可追蹤性的差異。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="hash-function">hash function<a href="https://ignikah-dev.github.io/blog/short-url-design#hash-function" class="hash-link" aria-label="hash function的直接連結" title="hash function的直接連結" translate="no">​</a></h2>
<p>把原始 URL 投射到如 SHA256 或 MD5 的雜湊值，再取前面幾個字元做為短網址。這種做法的好處是每次只要有 URL 就能快速產生對應值，不需先寫入資料庫即可比較是否重複。但缺點也很明顯：</p>
<ul>
<li class="">縮短後缺乏唯一性保證，必須加上碰撞檢查與重試邏輯。</li>
<li class="">雜湊值仍承載原始 URL 的資訊片段，理論上可供暴力逆推（尤其選用太短時）。</li>
<li class="">無法表示建立順序、也不易追蹤來源，對一些流量分析情境不友善。</li>
</ul>
<p>若需求是臨時、開發測試用的短網址，hash 方案能快速上手；但正式服務需要加強碰撞處理與 idempotency，才能避免重複導向。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="id--base62">id + base62<a href="https://ignikah-dev.github.io/blog/short-url-design#id--base62" class="hash-link" aria-label="id + base62的直接連結" title="id + base62的直接連結" translate="no">​</a></h2>
<p>最常見的方式是讓資料庫自增 <code>id</code>，再把值轉成 base62（<code>0-9a-zA-Z</code>）。成品的特徵如下：</p>
<ul>
<li class="">唯一性：只要 <code>id</code> 不會重複，短網址就一定獨一無二。</li>
<li class="">可逆性：只要能轉回 base62，就能還原 <code>id</code>，進一步查出原始 URL。</li>
<li class="">長度可控：可以預先規劃 <code>id</code> 的最大範圍，轉成 base62 之後落在 6~8 個字元內。</li>
</ul>
<p>缺點是需要中心化的資料庫或序列器來協調 <code>id</code> 的分配。若要擴展到多個節點，有額外的分區或跨節點同步成本。此外，字串會隨時間遞增，若想躲避暴露資料量或流量模式，就需要加些混淆手法。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="snowflake--uuidv7--base62">snowflake / uuidv7 + base62<a href="https://ignikah-dev.github.io/blog/short-url-design#snowflake--uuidv7--base62" class="hash-link" aria-label="snowflake / uuidv7 + base62的直接連結" title="snowflake / uuidv7 + base62的直接連結" translate="no">​</a></h2>
<p>進階系統會採用分散式序列產生器，例如 Twitter 的 snowflake 或新的 uuidv7，它們把時間戳、節點編號、序號組合起來，再轉成 base62。這類方案的優勢包括：</p>
<ul>
<li class="">分散式：各節點可以獨立生成 id，不需集中鎖定。</li>
<li class="">可排序：產生順序可反映時間（尤其 uuidv7 與 snowflake 的時間前綴）。</li>
<li class="">長度合理、碰撞機率接近零。</li>
</ul>
<p>將這種 id 再 encode 成 base62，就能在不犧牲可排序性的前提下得到可分享的短網址。如果想更短，可以只取時間戳與序號部分、再加 checksum，降低使用者手動輸入錯誤的風險。這類做法適合高吞吐、跨區域部署或需要追蹤來源的產品級服務。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="整體架構要素">整體架構要素<a href="https://ignikah-dev.github.io/blog/short-url-design#%E6%95%B4%E9%AB%94%E6%9E%B6%E6%A7%8B%E8%A6%81%E7%B4%A0" class="hash-link" aria-label="整體架構要素的直接連結" title="整體架構要素的直接連結" translate="no">​</a></h2>
<p>可以把短網址系統想成兩層：儲存層負責維護短碼與原始 URL 的對應，邏輯層負責產生 id、判斷歸戶、追蹤與排程。常見的架構要素包括：</p>
<ul>
<li class=""><strong>資料表設計</strong>：主表 <code>short_links</code> 包含 <code>id</code>、<code>short_code</code>、<code>target_url</code>、<code>creator_id</code>、<code>created_at</code>、<code>expires_at</code>、<code>status</code>（例如 active/disabled）、<code>usage_count</code>、<code>last_accessed_at</code>。若衍生功能如 A/B 測試或廣告導流，可再加 <code>campaign_id</code>、<code>variant_tag</code>。建議以 <code>short_code</code> 為唯一索引，加速查找。</li>
<li class=""><strong>解析與 redirect</strong>：解析層收到短碼後，先查資料並更新 <code>usage_count</code>/<code>last_accessed_at</code>，再回傳 302 redirect 到 <code>target_url</code>。這個流程可以部署在 edge 或 CDN 層，盡量維持低延遲；複雜的條件（驗證、廣告插頁）可交由後端服務處理。</li>
<li class=""><strong>HTTP 狀態選擇</strong>：302（Found）是常見選擇，即使施工中也能快速改回原 URL。若希望搜尋引擎自然索引原址，改用 301（Moved Permanently）。對於可能要調整的短網址，分別設定 <code>redirect_type</code> 欄位控制返回值。</li>
<li class=""><strong>快取與同步</strong>：由於查表頻率高，配合 Redis/L1 cache 快取 <code>short_code -&gt; target_url</code>，並設定 TTL。若有分散寫入，應設計事件或 stream（Kafka/Change Data Capture）同步至快取節點以免 stale。</li>
<li class=""><strong>廣告與觀察</strong>：若要放廣告或橫幅頁面，可以在 <code>redirect</code> 前插入檢查，例如 <code>campaign_id</code> 决定是否先導至吸引頁、再透過 client-side script 紀錄曝光後跳轉。也可在 <code>short_links</code> 加 <code>landing_page_html</code> 選項，讓某些短碼呈現自訂落地頁再跳轉。</li>
<li class=""><strong>追蹤與安全</strong>：加入 <code>referer</code>/<code>utm</code> 欄位、IP 黑名單、rate limit，讓濫用時能降載。另外可以記錄 <code>fingerprint</code> 或 <code>user_agent</code> 供分析使用頻率與地域。</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="總結">總結<a href="https://ignikah-dev.github.io/blog/short-url-design#%E7%B8%BD%E7%B5%90" class="hash-link" aria-label="總結的直接連結" title="總結的直接連結" translate="no">​</a></h2>
<p>若是單機、初期方案，<code>id + base62</code> 提供簡單可逆、長度易控的產出；想快速實驗或不想自己維護序列器，就可考慮 <code>hash function</code>（記得補上碰撞處理與 retry）。若系統將來會分散部署、需排序或高可用，<code>snowflake / uuidv7 + base62</code> 是比較周全的選項。也可以混搭，例如用 uuidv7 生成原始 id，再 encode 成 base62，最後加 checksum，兼顧易讀與健壯性。整體架構應涵蓋儲存表、快取、redirect 處理、狀態控制與廣告/觀察擴充，才能支援不同使用情境。</p>]]></content>
        <author>
            <name>Founder</name>
        </author>
        <category label="短網址" term="短網址"/>
        <category label="Hash Function" term="Hash Function"/>
        <category label="Base62" term="Base62"/>
        <category label="Snowflake ID" term="Snowflake ID"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[使用 MDX 打造互動式文件]]></title>
        <id>https://ignikah-dev.github.io/blog/docusaurus-mdx-demo</id>
        <link href="https://ignikah-dev.github.io/blog/docusaurus-mdx-demo"/>
        <updated>2025-11-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[為什麼選擇 MDX？]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="為什麼選擇-mdx">為什麼選擇 MDX？<a href="https://ignikah-dev.github.io/blog/docusaurus-mdx-demo#%E7%82%BA%E4%BB%80%E9%BA%BC%E9%81%B8%E6%93%87-mdx" class="hash-link" aria-label="為什麼選擇 MDX？的直接連結" title="為什麼選擇 MDX？的直接連結" translate="no">​</a></h2>
<p>當你需要說明重點提示、互動範例或分頁程式碼等更豐富的敘事方式時，可以在 Markdown 中混用 React 元件；而一般文字仍維持純 Markdown，兼顧易寫與彈性。</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_BuS1"><p>讓 MDX 區塊維持精簡（建議 40 行內），並替所有媒體提供描述性的 alt 文字，方便後續翻譯與在地化。</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="分頁內容範例">分頁內容範例<a href="https://ignikah-dev.github.io/blog/docusaurus-mdx-demo#%E5%88%86%E9%A0%81%E5%85%A7%E5%AE%B9%E7%AF%84%E4%BE%8B" class="hash-link" aria-label="分頁內容範例的直接連結" title="分頁內容範例的直接連結" translate="no">​</a></h2>
<div class="theme-tabs-container tabs-container tabList__CuJ"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_LNqP tabs__item--active">CLI 檢查清單</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">TypeScript 筆記</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_LNqP">Example</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_Ymn6"><ol>
<li class="">下載最新範本並執行 <code>npm install</code>。</li>
<li class="">透過 <code>npm run start</code> 在本機確認頁面載入正常。</li>
<li class="">將遇到的錯誤與重現步驟加到 issue 描述中。</li>
</ol></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><ul>
<li class="">先在 Markdown 定義所有可翻譯字串，再用 props 傳入 React 元件。</li>
<li class="">為 MDX 互動元件設計 <code>Props</code> 型別，方便其他作者複用。</li>
<li class="">若多篇文章都用到相同邏輯，可移到 <code>@site/src/components/</code> 匯入。</li>
</ul></div><div role="tabpanel" class="tabItem_Ymn6" hidden=""><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">&lt;Tabs groupId="language"&gt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    &lt;TabItem value="checklist" label="CLI 檢查清單"&gt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        1. 下載最新範本並執行 `npm install`。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        2. 透過 `npm run start` 在本機確認頁面載入正常。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        3. 將遇到的錯誤與重現步驟加到 issue 描述中。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    &lt;/TabItem&gt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    &lt;TabItem value="notes" label="TypeScript 筆記"&gt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        - 先在 Markdown 定義所有可翻譯字串，再用 props 傳入 React 元件。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        - 為 MDX 互動元件設計 `Props` 型別，方便其他作者複用。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        - 若多篇文章都用到相同邏輯，可移到 `@site/src/components/` 匯入。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    &lt;/TabItem&gt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">&lt;/Tabs&gt;</span><br></div></code></pre></div></div></div></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="可用的主題元件">可用的主題元件<a href="https://ignikah-dev.github.io/blog/docusaurus-mdx-demo#%E5%8F%AF%E7%94%A8%E7%9A%84%E4%B8%BB%E9%A1%8C%E5%85%83%E4%BB%B6" class="hash-link" aria-label="可用的主題元件的直接連結" title="可用的主題元件的直接連結" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://docusaurus.io/docs/markdown-features/tabs" target="_blank" rel="noopener noreferrer" class=""><code>Tabs</code> 與 <code>TabItem</code></a> 能在同一區塊切換語言或平台</li>
<li class=""><a href="https://docusaurus.io/docs/markdown-features/code-blocks#mdx-component" target="_blank" rel="noopener noreferrer" class=""><code>CodeBlock</code></a> 方便加上區塊標題或切成 live editor</li>
<li class=""><a href="https://docusaurus.io/docs/markdown-features/admonitions" target="_blank" rel="noopener noreferrer" class=""><code>Admonition</code></a> 與 <a href="https://docusaurus.io/docs/markdown-features/details#mdx" target="_blank" rel="noopener noreferrer" class=""><code>Details</code></a> 可凸顯警示或折疊補充說明</li>
</ul>]]></content>
        <author>
            <name>Founder</name>
        </author>
        <category label="Docusaurus" term="Docusaurus"/>
        <category label="MDX" term="MDX"/>
        <category label="文件" term="文件"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[前端常見測試框架比較：Vitest、Jest、Karma、Jasmine]]></title>
        <id>https://ignikah-dev.github.io/blog/frontend-test-runner-comparison</id>
        <link href="https://ignikah-dev.github.io/blog/frontend-test-runner-comparison"/>
        <updated>2025-11-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[以實務需求角度比較四個常見前端測試框架，協助你選擇適合的工具鏈。]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="為什麼要比較測試框架">為什麼要比較測試框架？<a href="https://ignikah-dev.github.io/blog/frontend-test-runner-comparison#%E7%82%BA%E4%BB%80%E9%BA%BC%E8%A6%81%E6%AF%94%E8%BC%83%E6%B8%AC%E8%A9%A6%E6%A1%86%E6%9E%B6" class="hash-link" aria-label="為什麼要比較測試框架？的直接連結" title="為什麼要比較測試框架？的直接連結" translate="no">​</a></h2>
<p>團隊在規劃測試策略時，往往需要兼顧開發體驗、既有 CI/CD 工具與瀏覽器支援度。這篇文章整理 Vitest、Jest、Karma 與 Jasmine 的定位差異，讓你快速判斷哪一套最符合專案需求。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="核心差異一覽">核心差異一覽<a href="https://ignikah-dev.github.io/blog/frontend-test-runner-comparison#%E6%A0%B8%E5%BF%83%E5%B7%AE%E7%95%B0%E4%B8%80%E8%A6%BD" class="hash-link" aria-label="核心差異一覽的直接連結" title="核心差異一覽的直接連結" translate="no">​</a></h2>
<table><thead><tr><th>框架</th><th>核心定位</th><th>執行環境</th><th>亮點</th><th>適合情境</th></tr></thead><tbody><tr><td>Vitest</td><td>Vite 生態系預設測試框架</td><td>Node.js（可透過 Vitest UI 觸發瀏覽器）</td><td>與 Vite 設定共享、原生 ESM、啟動極快</td><td>使用 Vite/Vue/React 並追求極速回饋的專案</td></tr><tr><td>Jest</td><td>Facebook 推出的通用測試框架</td><td>Node.js、JSDOM</td><td>Snapshot 測試、模組模擬、社群資源豐富</td><td>需要穩定 API、搭配 React/React Native 的團隊</td></tr><tr><td>Karma</td><td>利用真實瀏覽器跑測試的 Test Runner</td><td>真實瀏覽器（Chrome、Firefox 等）</td><td>可整合 Webpack/SystemJS，適合舊專案</td><td>必須驗證瀏覽器 API 或需要大量整合測試</td></tr><tr><td>Jasmine</td><td>早期 BDD 風格測試框架</td><td>瀏覽器或 Node.js</td><td>零依賴、語法簡潔</td><td>嵌入式或低依賴場景、AngularJS 舊專案</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>備註</div><div class="admonitionContent_BuS1"><p>若專案使用 Angular 14+ 並維持 CLI 預設，通常會同時搭配 Karma + Jasmine；若改用 Vite 驅動專案，就可考慮 Vitest 取代 Jest/Karma。</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="快速評估指南">快速評估指南<a href="https://ignikah-dev.github.io/blog/frontend-test-runner-comparison#%E5%BF%AB%E9%80%9F%E8%A9%95%E4%BC%B0%E6%8C%87%E5%8D%97" class="hash-link" aria-label="快速評估指南的直接連結" title="快速評估指南的直接連結" translate="no">​</a></h2>
<ol>
<li class=""><strong>優先考慮開發體驗</strong>：需要快速回饋與原生 ESM？Vitest 幾乎零設定，且能共用 Vite alias。</li>
<li class=""><strong>看重生態與教學資源</strong>：Jest 在社群套件、文件、CI 範例上仍最完整，適合多語言團隊。</li>
<li class=""><strong>要測到真實瀏覽器</strong>：Karma 擅長打開多個瀏覽器並收集覆蓋率，對 Web API、Legacy 專案仍有價值。</li>
<li class=""><strong>依賴最少、可嵌入</strong>：Jasmine 沒有外部 runner，但可輕鬆嵌入自訂腳本或 Karma、Protractor 等工具。</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="實務選型建議">實務選型建議<a href="https://ignikah-dev.github.io/blog/frontend-test-runner-comparison#%E5%AF%A6%E5%8B%99%E9%81%B8%E5%9E%8B%E5%BB%BA%E8%AD%B0" class="hash-link" aria-label="實務選型建議的直接連結" title="實務選型建議的直接連結" translate="no">​</a></h2>
<ul>
<li class=""><strong>Vite + Vue/React 新專案</strong>：直接選 Vitest，搭配 <code>vitest run --coverage</code> 即可整合 CI。</li>
<li class=""><strong>Next.js 或 React Native</strong>：Jest 有現成的環境模擬與 Snapshot 工具，且能與 Testing Library 無縫整合。</li>
<li class=""><strong>長期維護的 AngularJS/Angular 專案</strong>：保留 Karma + Jasmine，逐步以 Web Test Runner 或 Vitest 替換即可。</li>
<li class=""><strong>需要真實瀏覽器 E2E 但不想導入 Cypress/Playwright</strong>：Karma + Jasmine 仍是輕量選擇，可與 WebDriver 共享設定。</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="總結">總結<a href="https://ignikah-dev.github.io/blog/frontend-test-runner-comparison#%E7%B8%BD%E7%B5%90" class="hash-link" aria-label="總結的直接連結" title="總結的直接連結" translate="no">​</a></h2>
<p>選擇測試框架時請先盤點：</p>
<ul>
<li class="">Build 工具（Vite、Webpack、Angular CLI）</li>
<li class="">目標執行環境（Node、瀏覽器、Hybrid）</li>
<li class="">期望維護成本（社群範例、既有腳本）</li>
</ul>
<p>只要釐清上述條件，就能在 Vitest、Jest、Karma、Jasmine 之間做出更符合團隊節奏的決策。</p>]]></content>
        <author>
            <name>Founder</name>
        </author>
        <category label="Testing" term="Testing"/>
        <category label="Vitest" term="Vitest"/>
        <category label="Jest" term="Jest"/>
        <category label="Karma" term="Karma"/>
        <category label="Jasmine" term="Jasmine"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[在 WebStorm 中整合 Codex 工作流程]]></title>
        <id>https://ignikah-dev.github.io/blog/webstorm-codex-setup</id>
        <link href="https://ignikah-dev.github.io/blog/webstorm-codex-setup"/>
        <updated>2025-11-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[教你如何在 WebStorm 透過 Codex CLI 快速完成指令、腳本與部落格撰寫工作。]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="什麼是-codex">什麼是 Codex？<a href="https://ignikah-dev.github.io/blog/webstorm-codex-setup#%E4%BB%80%E9%BA%BC%E6%98%AF-codex" class="hash-link" aria-label="什麼是 Codex？的直接連結" title="什麼是 Codex？的直接連結" translate="no">​</a></h2>
<p><a href="https://openai.com/zh-Hant/codex/" target="_blank" rel="noopener noreferrer" class="">Codex</a> 是 OpenAI 提供的程式碼生成助手，能解讀自然語言並輸出程式碼、指令或文字。本文示範如何在 WebStorm 中透過 Codex CLI 建立一個以對話驅動的工作流程。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="為什麼要在-ide-中使用-codex">為什麼要在 IDE 中使用 Codex？<a href="https://ignikah-dev.github.io/blog/webstorm-codex-setup#%E7%82%BA%E4%BB%80%E9%BA%BC%E8%A6%81%E5%9C%A8-ide-%E4%B8%AD%E4%BD%BF%E7%94%A8-codex" class="hash-link" aria-label="為什麼要在 IDE 中使用 Codex？的直接連結" title="為什麼要在 IDE 中使用 Codex？的直接連結" translate="no">​</a></h2>
<p>WebStorm 已經提供完善的 TypeScript/React 體驗，但若再結合 Codex CLI，就能以對話方式生成模板、整理文件或批次修改檔案。這讓日常的重複性動作（例如建立部落格、填寫 front matter、插入多國語系內容）都能在同一個 IDE 視窗完成。</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>提示</div><div class="admonitionContent_BuS1"><p>Codex 維持在同一個版本最容易除錯，建議專案以 <code>package.json</code> script 管理，例如 <code>"codex": "npx codex"</code>，避免團隊成員版本不一致。</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="安裝與初始設定">安裝與初始設定<a href="https://ignikah-dev.github.io/blog/webstorm-codex-setup#%E5%AE%89%E8%A3%9D%E8%88%87%E5%88%9D%E5%A7%8B%E8%A8%AD%E5%AE%9A" class="hash-link" aria-label="安裝與初始設定的直接連結" title="安裝與初始設定的直接連結" translate="no">​</a></h2>
<ol>
<li class="">先在系統層級安裝 Codex CLI：<!-- -->
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">npm install -g @openai/codex-cli</span><br></div></code></pre></div></div>
</li>
<li class="">在專案根目錄建立或更新 <code>.context/</code>（例如 <code>docusaurus-guidelines.md</code>），把前置規範寫入，並於每次執行前指示 Codex 先閱讀。</li>
<li class="">若想用固定指令呼叫 Codex，可在 <code>package.json</code> 中加入腳本（底層仍呼叫全域 <code>codex</code> 或 <code>npx codex</code>）：<!-- -->
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"scripts"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"codex"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"codex"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"codex:plan"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"codex --plan"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="在-webstorm-綁定-codex-指令">在 WebStorm 綁定 Codex 指令<a href="https://ignikah-dev.github.io/blog/webstorm-codex-setup#%E5%9C%A8-webstorm-%E7%B6%81%E5%AE%9A-codex-%E6%8C%87%E4%BB%A4" class="hash-link" aria-label="在 WebStorm 綁定 Codex 指令的直接連結" title="在 WebStorm 綁定 Codex 指令的直接連結" translate="no">​</a></h2>
<ol>
<li class="">打開 <strong>Run/Debug Configurations</strong>，新增 <strong>npm</strong> 或 <strong>Shell Script</strong>。</li>
<li class="">指定 Script 為 <code>codex</code>，Working directory 指向專案根目錄。</li>
<li class="">勾選 <strong>Activate tool window</strong> 讓結果出現在 Run 面板，方便複製貼上。</li>
<li class="">若想要快速輸入指令，可在 <strong>Keymap → Plugins → External Tools</strong> 為 <code>codex</code> 配快捷鍵，例如 <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>;</kbd>。</li>
</ol>
<p>透過以上設定，就能在任意檔案按一次快捷鍵，立即呼叫 Codex 完成編輯或回覆。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="常見工作流程">常見工作流程<a href="https://ignikah-dev.github.io/blog/webstorm-codex-setup#%E5%B8%B8%E8%A6%8B%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="常見工作流程的直接連結" title="常見工作流程的直接連結" translate="no">​</a></h2>
<ul>
<li class=""><strong>撰寫部落格</strong>：執行 <code>npm run codex -- "create blog on webstorm"</code>，Codex 會依照 <code>.context</code> 中的規範與當天日期產生檔案，再由你在 WebStorm 內調整。</li>
<li class=""><strong>批次重構</strong>：在終端切換到目標資料夾，讓 Codex 讀取檔案後輸入具體需求，例如「將所有 fetch 換成 axios」。</li>
<li class=""><strong>Docs QA</strong>：透過 <code>codex --plan</code> 整理多步驟修改，把輸出貼進 <code>.mdx</code> 檔案，並利用 WebStorm Diff 介面快速檢查。做法是先在終端執行 <code>npm run codex:plan -- \"&lt;需求描述&gt;\"</code>，依序執行計畫並將 Codex 產生的內容貼回對應文件，最後用 Diff 檢查每一步是否符合預期。</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="疑難排解">疑難排解<a href="https://ignikah-dev.github.io/blog/webstorm-codex-setup#%E7%96%91%E9%9B%A3%E6%8E%92%E8%A7%A3" class="hash-link" aria-label="疑難排解的直接連結" title="疑難排解的直接連結" translate="no">​</a></h2>
<ul>
<li class=""><strong>終端無法解析 codex 指令</strong>：確認 WebStorm 使用的 Node 版本與系統一致，或在設定中的 Shell path 改成 <code>/bin/zsh -l</code>。</li>
<li class=""><strong>CLI 無法寫入檔案</strong>：在 Codex 命令前加入 <code>CODEx_SANDBOX=workspace-write</code>（依專案需求）或檢查 repository 權限。</li>
<li class=""><strong>輸出語言錯誤</strong>：把「預設使用繁體中文」寫在 <code>.context</code>，並提醒 Codex 每次編輯前重新閱讀該檔案。</li>
</ul>
<p>善用這套流程，就能在 WebStorm 中持續保持專注，同時享受到 Codex 的自動化與解題效率。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="其他可搭配的工具">其他可搭配的工具<a href="https://ignikah-dev.github.io/blog/webstorm-codex-setup#%E5%85%B6%E4%BB%96%E5%8F%AF%E6%90%AD%E9%85%8D%E7%9A%84%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="其他可搭配的工具的直接連結" title="其他可搭配的工具的直接連結" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://platform.claude.com/" target="_blank" rel="noopener noreferrer" class="">Claude</a>：在需要長篇推理或分析文件時可當作補充顧問。</li>
<li class=""><a href="https://kiro.dev/" target="_blank" rel="noopener noreferrer" class="">Kiro</a>：主打工程自動化，可接續 Codex 的結果進行端到端測試或部署。</li>
</ul>]]></content>
        <author>
            <name>Founder</name>
        </author>
        <category label="Codex" term="Codex"/>
        <category label="WebStorm" term="WebStorm"/>
        <category label="自動化" term="自動化"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[將多個 Nx Apps 部署到同一個 Nginx 下]]></title>
        <id>https://ignikah-dev.github.io/blog/2025/11/28</id>
        <link href="https://ignikah-dev.github.io/blog/2025/11/28"/>
        <updated>2025-11-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[教你如何將 Nx monorepo 內多個 Angular / Web apps 透過 Nginx 部署到同一個域名或不同路徑下。]]></summary>
        <content type="html"><![CDATA[<p>Nx monorepo 內通常包含多個前端或後端應用，例如：</p>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">apps/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  app1/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  app2/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  app3/</span><br></div></code></pre></div></div>
<p>透過 Nx build 後，你會得到：</p>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">dist/apps/app1/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dist/apps/app2/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dist/apps/app3/</span><br></div></code></pre></div></div>
<p>本篇文章將教你如何把這些 apps 同時部署到同一個 Nginx 下，並使用不同路徑或不同子網域。</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="方法-a使用不同路徑最常見">方法 A：使用不同路徑（最常見）<a href="https://ignikah-dev.github.io/blog/2025/11/28#%E6%96%B9%E6%B3%95-a%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%91%E6%9C%80%E5%B8%B8%E8%A6%8B" class="hash-link" aria-label="方法 A：使用不同路徑（最常見）的直接連結" title="方法 A：使用不同路徑（最常見）的直接連結" translate="no">​</a></h2>
<p>例如：</p>
<ul>
<li class=""><a href="https://example.com/app1" target="_blank" rel="noopener noreferrer" class="">https://example.com/app1</a></li>
<li class=""><a href="https://example.com/app2" target="_blank" rel="noopener noreferrer" class="">https://example.com/app2</a></li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="nginx-設定">Nginx 設定<a href="https://ignikah-dev.github.io/blog/2025/11/28#nginx-%E8%A8%AD%E5%AE%9A" class="hash-link" aria-label="Nginx 設定的直接連結" title="Nginx 設定的直接連結" translate="no">​</a></h3>
<div class="language-nginx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nginx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">server {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  listen 80;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  server_name example.com;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  location /app1/ {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    alias /usr/share/nginx/html/app1/;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    try_files $uri $uri/ /app1/index.html;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  location /app2/ {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    alias /usr/share/nginx/html/app2/;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    try_files $uri $uri/ /app2/index.html;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="angular--nx-build-指令">Angular / Nx Build 指令<a href="https://ignikah-dev.github.io/blog/2025/11/28#angular--nx-build-%E6%8C%87%E4%BB%A4" class="hash-link" aria-label="Angular / Nx Build 指令的直接連結" title="Angular / Nx Build 指令的直接連結" translate="no">​</a></h3>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">nx build app1 --configuration production --base-href=/app1/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nx build app2 --configuration production --base-href=/app2/</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="部署的資料夾結構">部署的資料夾結構<a href="https://ignikah-dev.github.io/blog/2025/11/28#%E9%83%A8%E7%BD%B2%E7%9A%84%E8%B3%87%E6%96%99%E5%A4%BE%E7%B5%90%E6%A7%8B" class="hash-link" aria-label="部署的資料夾結構的直接連結" title="部署的資料夾結構的直接連結" translate="no">​</a></h3>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">/usr/share/nginx/html/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  app1/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  app2/</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="方法-b使用不同子網域">方法 B：使用不同子網域<a href="https://ignikah-dev.github.io/blog/2025/11/28#%E6%96%B9%E6%B3%95-b%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%90%8C%E5%AD%90%E7%B6%B2%E5%9F%9F" class="hash-link" aria-label="方法 B：使用不同子網域的直接連結" title="方法 B：使用不同子網域的直接連結" translate="no">​</a></h2>
<p>例如：</p>
<ul>
<li class=""><a href="https://app1.example.com/" target="_blank" rel="noopener noreferrer" class="">https://app1.example.com</a></li>
<li class=""><a href="https://app2.example.com/" target="_blank" rel="noopener noreferrer" class="">https://app2.example.com</a></li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="nginx-設定-1">Nginx 設定<a href="https://ignikah-dev.github.io/blog/2025/11/28#nginx-%E8%A8%AD%E5%AE%9A-1" class="hash-link" aria-label="Nginx 設定的直接連結" title="Nginx 設定的直接連結" translate="no">​</a></h3>
<div class="language-nginx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nginx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">server {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  server_name app1.example.com;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  root /usr/share/nginx/html/app1;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  try_files $uri $uri/ /index.html;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">server {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  server_name app2.example.com;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  root /usr/share/nginx/html/app2;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  try_files $uri $uri/ /index.html;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="angular--nx-build-指令-1">Angular / Nx Build 指令<a href="https://ignikah-dev.github.io/blog/2025/11/28#angular--nx-build-%E6%8C%87%E4%BB%A4-1" class="hash-link" aria-label="Angular / Nx Build 指令的直接連結" title="Angular / Nx Build 指令的直接連結" translate="no">​</a></h3>
<p>由於每個 app 直接掛在根目錄（/），<code>baseHref</code> 設為 <code>/</code> 即可：</p>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">nx build app1 --configuration production --base-href=/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nx build app2 --configuration production --base-href=/</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="方法-c透過-proxy-pass-導流到後端服務api--ssr--node--nodejs">方法 C：透過 Proxy Pass 導流到後端服務（API / SSR / Node / NodeJS）<a href="https://ignikah-dev.github.io/blog/2025/11/28#%E6%96%B9%E6%B3%95-c%E9%80%8F%E9%81%8E-proxy-pass-%E5%B0%8E%E6%B5%81%E5%88%B0%E5%BE%8C%E7%AB%AF%E6%9C%8D%E5%8B%99api--ssr--node--nodejs" class="hash-link" aria-label="方法 C：透過 Proxy Pass 導流到後端服務（API / SSR / Node / NodeJS）的直接連結" title="方法 C：透過 Proxy Pass 導流到後端服務（API / SSR / Node / NodeJS）的直接連結" translate="no">​</a></h2>
<p>如果你的 Nx monorepo 中包含後端服務（例如 NodeJS API），你可以使用 <code>proxy_pass</code>：
讓 Nginx 將 <code>/api</code> 相關請求導向後端 server。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="nginx-proxy-pass-設定範例">Nginx Proxy Pass 設定範例<a href="https://ignikah-dev.github.io/blog/2025/11/28#nginx-proxy-pass-%E8%A8%AD%E5%AE%9A%E7%AF%84%E4%BE%8B" class="hash-link" aria-label="Nginx Proxy Pass 設定範例的直接連結" title="Nginx Proxy Pass 設定範例的直接連結" translate="no">​</a></h3>
<div class="language-nginx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nginx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">server {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  listen 80;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  server_name example.com;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  # 前端 Angular App</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  location / {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    root /usr/share/nginx/html/app1;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    try_files $uri $uri/ /index.html;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  # API Proxy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  location /api/ {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    proxy_pass http://localhost:3333/; # Nx serve / NodeJS 的 API port</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    proxy_http_version 1.1;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    proxy_set_header Upgrade $http_upgrade;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    proxy_set_header Connection 'upgrade';</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    proxy_set_header Host $host;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    proxy_cache_bypass $http_upgrade;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="常見-proxy-pass-場景">常見 Proxy Pass 場景<a href="https://ignikah-dev.github.io/blog/2025/11/28#%E5%B8%B8%E8%A6%8B-proxy-pass-%E5%A0%B4%E6%99%AF" class="hash-link" aria-label="常見 Proxy Pass 場景的直接連結" title="常見 Proxy Pass 場景的直接連結" translate="no">​</a></h3>
<ol>
<li class="">
<p><strong>Nx + Angular 前端 + NodeJS API</strong></p>
<ul>
<li class=""><code>/</code> → 前端</li>
<li class=""><code>/api</code> → 後端（NodeJS）</li>
</ul>
</li>
<li class="">
<p><strong>多後端服務（微服務結構）</strong></p>
<ul>
<li class=""><code>/auth</code> → Auth service</li>
<li class=""><code>/order</code> → Order service</li>
<li class=""><code>/payment</code> → Payment service</li>
</ul>
</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="nx--nodejs-服務常用指令">Nx + NodeJS 服務常用指令<a href="https://ignikah-dev.github.io/blog/2025/11/28#nx--nodejs-%E6%9C%8D%E5%8B%99%E5%B8%B8%E7%94%A8%E6%8C%87%E4%BB%A4" class="hash-link" aria-label="Nx + NodeJS 服務常用指令的直接連結" title="Nx + NodeJS 服務常用指令的直接連結" translate="no">​</a></h3>
<div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">nx serve api</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nx build api</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="常見問題faq">常見問題（FAQ）<a href="https://ignikah-dev.github.io/blog/2025/11/28#%E5%B8%B8%E8%A6%8B%E5%95%8F%E9%A1%8Cfaq" class="hash-link" aria-label="常見問題（FAQ）的直接連結" title="常見問題（FAQ）的直接連結" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="為什麼重新整理會出現-404">為什麼重新整理會出現 404？<a href="https://ignikah-dev.github.io/blog/2025/11/28#%E7%82%BA%E4%BB%80%E9%BA%BC%E9%87%8D%E6%96%B0%E6%95%B4%E7%90%86%E6%9C%83%E5%87%BA%E7%8F%BE-404" class="hash-link" aria-label="為什麼重新整理會出現 404？的直接連結" title="為什麼重新整理會出現 404？的直接連結" translate="no">​</a></h3>
<p>SPA（如 Angular）本身沒有真正的路徑，因此 Nginx 需要設定：</p>
<div class="language-nginx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nginx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">try_files $uri $uri/ /index.html;</span><br></div></code></pre></div></div>
<p>這會讓所有路徑回到 Angular 的 router 處理。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="proxy-pass-無法連線">Proxy Pass 無法連線？<a href="https://ignikah-dev.github.io/blog/2025/11/28#proxy-pass-%E7%84%A1%E6%B3%95%E9%80%A3%E7%B7%9A" class="hash-link" aria-label="Proxy Pass 無法連線？的直接連結" title="Proxy Pass 無法連線？的直接連結" translate="no">​</a></h3>
<p>檢查：</p>
<ul>
<li class="">後端 port 是否正確（如 <code>3333</code>）</li>
<li class="">後端是否允許來自 localhost 的 request</li>
<li class="">Nginx 有沒有加 <code>proxy_set_header Host $host</code></li>
<li class="">是否需要 HTTPS → HTTP 的 <code>proxy_redirect off;</code></li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="總結">總結<a href="https://ignikah-dev.github.io/blog/2025/11/28#%E7%B8%BD%E7%B5%90" class="hash-link" aria-label="總結的直接連結" title="總結的直接連結" translate="no">​</a></h2>
<p>你可以透過三種方式把多個 Nx apps 與後端部署在同一個 Nginx：</p>
<ol>
<li class=""><strong>不同路徑</strong>（最常見、單一 domain）</li>
<li class=""><strong>不同子網域</strong>（乾淨、好管理）</li>
<li class=""><strong>Proxy Pass 導後端 API</strong>（前後端整合最佳解）</li>
</ol>
<p>只要正確設定：</p>
<ul>
<li class="">Nginx alias/root</li>
<li class="">Angular baseHref</li>
<li class="">try_files</li>
<li class="">proxy_pass（如有後端 API）</li>
</ul>
<p>即可達成完整的 Nx monorepo 部署架構。</p>]]></content>
        <category label="Nx" term="Nx"/>
        <category label="Angular" term="Angular"/>
        <category label="Nginx" term="Nginx"/>
        <category label="部署" term="部署"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Nx App 拆分優勢與策略步驟]]></title>
        <id>https://ignikah-dev.github.io/blog/nx-app-split-strategy</id>
        <link href="https://ignikah-dev.github.io/blog/nx-app-split-strategy"/>
        <updated>2025-11-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[以最安全的 Clone & Carve 策略，將單一 Angular Nx App 拆分為前台與後台兩個獨立應用，並可保留原有路由與逐步驗證。]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-背景說明">1. 背景說明<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#1-%E8%83%8C%E6%99%AF%E8%AA%AA%E6%98%8E" class="hash-link" aria-label="1. 背景說明的直接連結" title="1. 背景說明的直接連結" translate="no">​</a></h2>
<p>目前前端專案採用 Nx monorepo，原本僅有單一應用程式 <code>shopping-app</code>，同時承載：</p>
<ul>
<li class="">一般使用者（前台）功能</li>
<li class="">管理者／營運人員（後台）功能</li>
</ul>
<p>隨著功能擴充與角色複雜度提升，單一 App 逐漸出現以下問題：</p>
<ul>
<li class="">bundle 體積愈來愈大，首屏載入時間增加</li>
<li class="">測試與除錯範圍變廣，改一小段功能要驗證很多頁面</li>
<li class="">權限、導覽、UI 風格混雜在一起，維護成本高</li>
<li class="">部署策略無法細緻，例如：只想更新後台功能但仍須重建整個 App</li>
</ul>
<p>為解決上述問題，我們規劃將前端拆分為兩個獨立 App，並保留既有路由邏輯與使用者習慣。</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-拆成兩個-app-的優勢">2. 拆成兩個 App 的優勢<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#2-%E6%8B%86%E6%88%90%E5%85%A9%E5%80%8B-app-%E7%9A%84%E5%84%AA%E5%8B%A2" class="hash-link" aria-label="2. 拆成兩個 App 的優勢的直接連結" title="2. 拆成兩個 App 的優勢的直接連結" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="21-架構與責任分離">2.1 架構與責任分離<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#21-%E6%9E%B6%E6%A7%8B%E8%88%87%E8%B2%AC%E4%BB%BB%E5%88%86%E9%9B%A2" class="hash-link" aria-label="2.1 架構與責任分離的直接連結" title="2.1 架構與責任分離的直接連結" translate="no">​</a></h3>
<p>拆分後我們會有兩個應用：</p>
<ul>
<li class=""><code>shopping-app</code>：前台，面向一般使用者</li>
<li class=""><code>backoffice-app</code>：後台，面向管理者／營運人員</li>
</ul>
<p>優勢：</p>
<ul>
<li class="">前後台的路由、版型、權限可以完全分開設計</li>
<li class="">開發時不會被另一個角色的 UI 汙染</li>
<li class="">規格討論可以針對各自 App 進行，不互相牽扯</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="22-打包與建置時間優化">2.2 打包與建置時間優化<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#22-%E6%89%93%E5%8C%85%E8%88%87%E5%BB%BA%E7%BD%AE%E6%99%82%E9%96%93%E5%84%AA%E5%8C%96" class="hash-link" aria-label="2.2 打包與建置時間優化的直接連結" title="2.2 打包與建置時間優化的直接連結" translate="no">​</a></h3>
<p>在 Nx 下，拆分多個 App 可以搭配：</p>
<ul>
<li class="">incremental build（增量編譯）</li>
<li class="">affected:* 指令（只建置有受影響的專案）</li>
</ul>
<p>實際效果：</p>
<ul>
<li class="">只修改後台程式碼時，可僅建置 <code>backoffice-app</code></li>
<li class="">前台沒有變動就不需重新 build，CI 時間與成本下降</li>
<li class="">本地開發時可以只啟動自己關心的 App，啟動速度較快</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="23-bundle-大小與載入效能改善">2.3 bundle 大小與載入效能改善<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#23-bundle-%E5%A4%A7%E5%B0%8F%E8%88%87%E8%BC%89%E5%85%A5%E6%95%88%E8%83%BD%E6%94%B9%E5%96%84" class="hash-link" aria-label="2.3 bundle 大小與載入效能改善的直接連結" title="2.3 bundle 大小與載入效能改善的直接連結" translate="no">​</a></h3>
<ul>
<li class="">前台不再需要下載後台相關的頁面與元件</li>
<li class="">Admin 專用 UI / library 不會出現在前台 bundle 中</li>
<li class="">Lazy loading 策略可以更乾淨，依 App 設計懸掛不同的 chunk</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="24-權限與安全性清晰化">2.4 權限與安全性清晰化<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#24-%E6%AC%8A%E9%99%90%E8%88%87%E5%AE%89%E5%85%A8%E6%80%A7%E6%B8%85%E6%99%B0%E5%8C%96" class="hash-link" aria-label="2.4 權限與安全性清晰化的直接連結" title="2.4 權限與安全性清晰化的直接連結" translate="no">​</a></h3>
<ul>
<li class="">後台 App 可以在 routing 層、部署層各自設計權限控管</li>
<li class="">反向代理（nginx / gateway）可針對不同 path/domain 做額外保護</li>
<li class="">不讓 admin 專用的頁面隨前台一起被發佈</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="25-部署與版本獨立">2.5 部署與版本獨立<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#25-%E9%83%A8%E7%BD%B2%E8%88%87%E7%89%88%E6%9C%AC%E7%8D%A8%E7%AB%8B" class="hash-link" aria-label="2.5 部署與版本獨立的直接連結" title="2.5 部署與版本獨立的直接連結" translate="no">​</a></h3>
<p>未來可實現：</p>
<ul>
<li class="">前台與後台採用不同的發版節奏</li>
<li class="">僅更新後台功能時，不動到前台 bundle</li>
<li class="">甚至可分別部署到不同 domain 或子路徑</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-拆分核心策略clone--carve">3. 拆分核心策略：Clone &amp; Carve<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#3-%E6%8B%86%E5%88%86%E6%A0%B8%E5%BF%83%E7%AD%96%E7%95%A5clone--carve" class="hash-link" aria-label="3. 拆分核心策略：Clone &amp; Carve的直接連結" title="3. 拆分核心策略：Clone &amp; Carve的直接連結" translate="no">​</a></h2>
<p>本次拆分採用「Clone &amp; Carve」策略：</p>
<blockquote>
<p>先完整複製，再在複製版本中慢慢「削減」成新形狀，而不是直接改原本的 App。</p>
</blockquote>
<p>流程概念：</p>
<ol>
<li class="">先複製現有 App → 得到兩份一模一樣的應用</li>
<li class="">確保兩個 App 都能正常 serve / build</li>
<li class="">在新 App 中逐步刪除不需要的功能頁面</li>
<li class="">建立路由／部署分流</li>
<li class="">拆完後，再逐步抽出共用的 libs</li>
</ol>
<p>好處：</p>
<ul>
<li class="">原本的 <code>shopping-app</code> 在拆分初期完全不被動到</li>
<li class="">出現問題可以隨時回退到舊的單一 App</li>
<li class="">每個步驟都可以單獨測試與驗證</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-視覺化流程與目錄結構">4. 視覺化流程與目錄結構<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#4-%E8%A6%96%E8%A6%BA%E5%8C%96%E6%B5%81%E7%A8%8B%E8%88%87%E7%9B%AE%E9%8C%84%E7%B5%90%E6%A7%8B" class="hash-link" aria-label="4. 視覺化流程與目錄結構的直接連結" title="4. 視覺化流程與目錄結構的直接連結" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="41-流程圖文字版">4.1 流程圖（文字版）<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#41-%E6%B5%81%E7%A8%8B%E5%9C%96%E6%96%87%E5%AD%97%E7%89%88" class="hash-link" aria-label="4.1 流程圖（文字版）的直接連結" title="4.1 流程圖（文字版）的直接連結" translate="no">​</a></h3>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">[現有 shopping-app]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[複製為 backoffice-app]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[確認兩者可獨立 serve &amp; build]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[在 backoffice-app 內刪除前台頁面]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[建立兩者 routing / 部署分流]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[未來再抽出 shared libs]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[Build / 部署 / 權限 全面分離]</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="42-目錄結構變更">4.2 目錄結構變更<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#42-%E7%9B%AE%E9%8C%84%E7%B5%90%E6%A7%8B%E8%AE%8A%E6%9B%B4" class="hash-link" aria-label="4.2 目錄結構變更的直接連結" title="4.2 目錄結構變更的直接連結" translate="no">​</a></h3>
<p>拆分前：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">apps/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  shopping-app/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pages/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      home</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      note</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      article</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      tag</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      search</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      backoffice</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      admin</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      transcript</span><br></div></code></pre></div></div>
<p>拆分後：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">apps/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  shopping-app/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pages/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      home</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      note</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      article</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      tag</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      search</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  backoffice-app/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pages/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      backoffice</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      admin</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      transcript</span><br></div></code></pre></div></div>
<p>未來再視需求抽出 libs：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">libs/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  shared-model</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  shared-api</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  shared-domain</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  shared-ui</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  shared-utils</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-實作步驟操作手冊">5. 實作步驟（操作手冊）<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#5-%E5%AF%A6%E4%BD%9C%E6%AD%A5%E9%A9%9F%E6%93%8D%E4%BD%9C%E6%89%8B%E5%86%8A" class="hash-link" aria-label="5. 實作步驟（操作手冊）的直接連結" title="5. 實作步驟（操作手冊）的直接連結" translate="no">​</a></h2>
<p>以下步驟以 Nx + Angular 為例，重點在「最小異動」與「可隨時回退」。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="51-複製現有-app">5.1 複製現有 App<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#51-%E8%A4%87%E8%A3%BD%E7%8F%BE%E6%9C%89-app" class="hash-link" aria-label="5.1 複製現有 App的直接連結" title="5.1 複製現有 App的直接連結" translate="no">​</a></h3>
<ol>
<li class="">
<p>在 repo 根目錄複製：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">cp -R apps/shopping-app apps/backoffice-app</span><br></div></code></pre></div></div>
</li>
<li class="">
<p>確認複製後目錄存在：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">apps/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  shopping-app/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  backoffice-app/</span><br></div></code></pre></div></div>
</li>
</ol>
<p>此時兩個 App 的內容完全相同。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="511-nginx-proxy-導流測試在拆分前先做">5.1.1 Nginx Proxy 導流測試（在拆分前先做）<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#511-nginx-proxy-%E5%B0%8E%E6%B5%81%E6%B8%AC%E8%A9%A6%E5%9C%A8%E6%8B%86%E5%88%86%E5%89%8D%E5%85%88%E5%81%9A" class="hash-link" aria-label="5.1.1 Nginx Proxy 導流測試（在拆分前先做）的直接連結" title="5.1.1 Nginx Proxy 導流測試（在拆分前先做）的直接連結" translate="no">​</a></h3>
<div class="language-nginx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nginx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">server {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  listen 80;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  server_name yourdomain.com;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  # 前台路由</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  location / {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    proxy_pass http://localhost:4200;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  # 後台路由</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  location /backoffice/ {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    proxy_pass http://localhost:4300;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  # 避免 Angular 路由錯誤</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  location /backoffice {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    return 301 /backoffice/;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>說明：</p>
<ul>
<li class="">在拆 App 之前，就先建立 nginx proxy 路由</li>
<li class="">nginx 充當反向代理，幫助我們先驗證 path 分流</li>
<li class="">然後再複製 app 並建立 <code>backoffice-app</code></li>
</ul>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">[加上 nginx proxy → 建立 / 與 /backoffice 路由分流]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[確認分流可正常把流量導向不同開發 port]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        |</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[複製 shopping-app → backoffice-app]</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="52-調整-backoffice-專案設定">5.2 調整 backoffice 專案設定<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#52-%E8%AA%BF%E6%95%B4-backoffice-%E5%B0%88%E6%A1%88%E8%A8%AD%E5%AE%9A" class="hash-link" aria-label="5.2 調整 backoffice 專案設定的直接連結" title="5.2 調整 backoffice 專案設定的直接連結" translate="no">​</a></h3>
<p>在 <code>backoffice-app</code> 底下，調整下列檔案：</p>
<ol>
<li class="">
<p><code>project.json</code></p>
<ul>
<li class="">將 name 由 <code>shopping-app</code> 調整為 <code>backoffice-app</code></li>
<li class="">確定 <code>sourceRoot</code> 指向 <code>apps/backoffice-app/src</code></li>
</ul>
</li>
<li class="">
<p><code>tsconfig.app.json</code></p>
<ul>
<li class="">確認 <code>extends</code>、<code>files</code>、<code>include</code> 等路徑指向 backoffice-app</li>
</ul>
</li>
<li class="">
<p><code>index.html</code></p>
<ul>
<li class="">修改 <code>&lt;title&gt;</code> 例如：<code>Backoffice Admin</code></li>
</ul>
</li>
</ol>
<p>調整完成後，在本機執行：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">nx serve shopping-app</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nx serve backoffice-app</span><br></div></code></pre></div></div>
<p>確認兩個 App 都能啟動且畫面正常，即可進入下一步。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="53-在-backoffice-app-中刪除前台頁面">5.3 在 backoffice-app 中刪除前台頁面<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#53-%E5%9C%A8-backoffice-app-%E4%B8%AD%E5%88%AA%E9%99%A4%E5%89%8D%E5%8F%B0%E9%A0%81%E9%9D%A2" class="hash-link" aria-label="5.3 在 backoffice-app 中刪除前台頁面的直接連結" title="5.3 在 backoffice-app 中刪除前台頁面的直接連結" translate="no">​</a></h3>
<p>目標：在不影響 <code>shopping-app</code> 的前提下，慢慢把 <code>backoffice-app</code> 修剪成「只有後台功能」的應用。</p>
<p>做法：</p>
<ol>
<li class="">
<p>在 <code>backoffice-app</code> 中，先確定 routing 結構與檔案位置</p>
</li>
<li class="">
<p>針對明顯前台功能（例如：<code>home</code>, <code>note</code>, <code>article</code>, <code>tag</code>, <code>search</code>），依序：</p>
<ul>
<li class="">從 routing 設定中移除該 path</li>
<li class="">移除頁面元件檔案</li>
<li class="">移除對應的測試檔、style 等</li>
</ul>
</li>
<li class="">
<p>每移除一組路由／頁面，都進行一次：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">nx build backoffice-app</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nx serve backoffice-app</span><br></div></code></pre></div></div>
<p>確認：</p>
<ul>
<li class="">build 無錯誤</li>
<li class="">主要後台路徑（例如 <code>/backoffice</code>, <code>/admin</code>, <code>/transcript</code>）仍可正常運作</li>
</ul>
</li>
</ol>
<p>完成後，目錄大致會變成：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">apps/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  backoffice-app/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pages/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      backoffice</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      admin</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      transcript</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="54-建立路由與部署分流">5.4 建立路由與部署分流<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#54-%E5%BB%BA%E7%AB%8B%E8%B7%AF%E7%94%B1%E8%88%87%E9%83%A8%E7%BD%B2%E5%88%86%E6%B5%81" class="hash-link" aria-label="5.4 建立路由與部署分流的直接連結" title="5.4 建立路由與部署分流的直接連結" translate="no">​</a></h3>
<p>在 gateway / nginx / 前端部署設定中，將路由切開，例如：</p>
<ul>
<li class=""><code>/</code> 或 <code>/app</code> → 指向 <code>shopping-app</code> build 出來的 bundle</li>
<li class=""><code>/backoffice</code> 或 <code>/admin</code> → 指向 <code>backoffice-app</code> build 出來的 bundle</li>
</ul>
<p>如此一來：</p>
<ul>
<li class="">使用者造訪前台時，不會載入後台的程式碼</li>
<li class="">後台可以獨立演進、獨立測試</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="55-拆分完成後的共用程式抽離後期">5.5 拆分完成後的共用程式抽離（後期）<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#55-%E6%8B%86%E5%88%86%E5%AE%8C%E6%88%90%E5%BE%8C%E7%9A%84%E5%85%B1%E7%94%A8%E7%A8%8B%E5%BC%8F%E6%8A%BD%E9%9B%A2%E5%BE%8C%E6%9C%9F" class="hash-link" aria-label="5.5 拆分完成後的共用程式抽離（後期）的直接連結" title="5.5 拆分完成後的共用程式抽離（後期）的直接連結" translate="no">​</a></h3>
<p>拆分前期不建議直接抽 libs，以免一次改動太大。</p>
<p>當兩個 App 穩定運作後，可以開始評估：</p>
<ul>
<li class="">共用的 model / DTO</li>
<li class="">共用的 API service</li>
<li class="">共用的 UI components</li>
<li class="">共用的 util / helper</li>
</ul>
<p>再逐步抽到 <code>libs/</code> 底下的 shared 專案中，搭配 Nx 提供的 dependency graph 逐步優化結構。</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-測試與風險控管">6. 測試與風險控管<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#6-%E6%B8%AC%E8%A9%A6%E8%88%87%E9%A2%A8%E9%9A%AA%E6%8E%A7%E7%AE%A1" class="hash-link" aria-label="6. 測試與風險控管的直接連結" title="6. 測試與風險控管的直接連結" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="61-建議的測試順序">6.1 建議的測試順序<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#61-%E5%BB%BA%E8%AD%B0%E7%9A%84%E6%B8%AC%E8%A9%A6%E9%A0%86%E5%BA%8F" class="hash-link" aria-label="6.1 建議的測試順序的直接連結" title="6.1 建議的測試順序的直接連結" translate="no">​</a></h3>
<p>每一個拆分步驟都建議至少做：</p>
<ol>
<li class="">單一 App build 測試：<!-- -->
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">nx build shopping-app</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nx build backoffice-app</span><br></div></code></pre></div></div>
</li>
<li class="">本地手動驗證關鍵路由：<!-- -->
<ul>
<li class="">前台：主要使用者流程是否正常</li>
<li class="">後台：登入、查詢、管理功能是否正常</li>
</ul>
</li>
<li class="">重要節點時執行 e2e / smoke test</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="62-風險與對應策略">6.2 風險與對應策略<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#62-%E9%A2%A8%E9%9A%AA%E8%88%87%E5%B0%8D%E6%87%89%E7%AD%96%E7%95%A5" class="hash-link" aria-label="6.2 風險與對應策略的直接連結" title="6.2 風險與對應策略的直接連結" translate="no">​</a></h3>
<table><thead><tr><th>風險類型</th><th>說明</th><th>對應策略</th></tr></thead><tbody><tr><td>build 爆炸</td><td>一次改太多檔案</td><td>每次只做小步驟，改完就 build 一次</td></tr><tr><td>路由錯亂</td><td>path 指到錯的 App</td><td>將前後台 routing 寫在文件與設定中統一管理</td></tr><tr><td>共用程式被誤刪</td><td>後台仍需要前台某段邏輯</td><td>初期只刪除「明顯純 UI」頁面，不動 domain / service</td></tr><tr><td>難以回退</td><td>多步驟混在同一 commit</td><td>每個重要步驟分開 commit，必要時可 git revert</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="7-結語">7. 結語<a href="https://ignikah-dev.github.io/blog/nx-app-split-strategy#7-%E7%B5%90%E8%AA%9E" class="hash-link" aria-label="7. 結語的直接連結" title="7. 結語的直接連結" translate="no">​</a></h2>
<p>本次 Nx App 拆分的目標，不是追求一次到位的「完美架構」，而是：</p>
<ol>
<li class="">在不影響現有使用者的情況下，先把前後台在應用層分離開來</li>
<li class="">降低 build 時間與 bundle 大小，改善開發與使用體驗</li>
<li class="">為未來的 libs 抽離與獨立部署鋪路</li>
</ol>
<p>核心心法：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Keep it working → Keep it separated → Keep it evolving</span><br></div></code></pre></div></div>
<p>先讓系統穩定分家，再持續演進結構，是目前最務實、風險最低的拆分路線。</p>]]></content>
        <author>
            <name>Founder</name>
        </author>
        <category label="Nx" term="Nx"/>
        <category label="Angular" term="Angular"/>
        <category label="架構" term="架構"/>
        <category label="Monorepo" term="Monorepo"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[n8n Course Level 1]]></title>
        <id>https://ignikah-dev.github.io/blog/n8n-course-level1</id>
        <link href="https://ignikah-dev.github.io/blog/n8n-course-level1"/>
        <updated>2025-11-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[跟著人生攻略研究所所長，一步步完成 n8n Level 1 測驗的紀錄。]]></summary>
        <content type="html"><![CDATA[<p>跟著人生攻略研究所所長的腳步，正式踏進 n8n Course Level 1 的測驗流程。<br>
<!-- -->整體難度不算高，但實際作答時，還是有幾題需要重新確認邏輯與分類方式。</p>
<p>事前準備按著所長的教學完成註冊與金鑰取得：<br>
<a href="https://lifecheatslab.com/n8n-course/#%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E8%A8%BB%E5%86%8A_Level_1_%E6%B8%AC%E9%A9%97%EF%BC%8C%E5%8F%96%E5%BE%97%E5%80%8B%E4%BA%BA%E9%87%91%E9%91%B0" target="_blank" rel="noopener noreferrer" class="">https://lifecheatslab.com/n8n-course/#第一步：註冊_Level_1_測驗，取得個人金鑰</a></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="理論題">理論題<a href="https://ignikah-dev.github.io/blog/n8n-course-level1#%E7%90%86%E8%AB%96%E9%A1%8C" class="hash-link" aria-label="理論題的直接連結" title="理論題的直接連結" translate="no">​</a></h3>
<p>在整份題庫裡，理論題的第 4 題與第 6 題特別容易讓人猶豫。</p>
<ul>
<li class="">
<p><strong>第 4 題</strong>：What can you do if there is no n8n node for an app/service that you want to use in a workflow?</p>
</li>
<li class="">
<p><strong>第 6 題</strong>：Which of the following are Trigger Nodes?<br>
<!-- -->依題目列出的選項來看，除了 Schedule node；Airtable node 實作上也可以。</p>
</li>
</ul>
<p>其他題目主要檢查對 n8n 基礎概念的掌握：哪些節點能啟動 workflow、Code node 必須回傳什麼格式、資料結構怎麼判讀等等。<br>
<!-- -->多利用官方文件與搜尋，大部分都能順利作答。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="實作題">實作題<a href="https://ignikah-dev.github.io/blog/n8n-course-level1#%E5%AF%A6%E4%BD%9C%E9%A1%8C" class="hash-link" aria-label="實作題的直接連結" title="實作題的直接連結" translate="no">​</a></h3>
<p>實作題的部分只要掌握流程邏輯，其實不算太複雜。<br>
<!-- -->最方便的方式，是先匯入官方提供的完成版 workflow，再依照題目逐步調整即可。</p>
<p>官方 workflow JSON：<br>
<a href="https://docs.n8n.io/_workflows/courses/level-one/finished.json" target="_blank" rel="noopener noreferrer" class="">https://docs.n8n.io/_workflows/courses/level-one/finished.json</a></p>
<p>匯入後，照題目要求調整各個節點：補上 IF 條件、調整資料格式、確認流程順序。<br>
<!-- -->Airtable 在這份測驗裡不需要真的去操作，直接略過就好，不會影響作答結果。<br>
<!-- -->整體流程清楚、節點之間的連動也好檢查，照著步驟實作很快就能完成。</p>
<p>搭配課程文件的補充說明：<br>
<a href="https://docs.n8n.io/courses/level-one/chapter-7/" target="_blank" rel="noopener noreferrer" class="">https://docs.n8n.io/courses/level-one/chapter-7/</a></p>
<p>雖然只是 Level 1，但完成後看到進度亮起來，還是有種踏出第一步的成就感。</p>
<p><img decoding="async" loading="lazy" alt="n8n-course-level1.png" src="https://ignikah-dev.github.io/assets/images/n8n-course-level1-c0cd91486111556753cc4d4c4626e4be.png" width="986" height="633" class="img_ev3q"></p>]]></content>
        <author>
            <name>Founder</name>
        </author>
        <category label="n8n Level 1" term="n8n Level 1"/>
    </entry>
</feed>