[{"data":1,"prerenderedAt":4184},["ShallowReactive",2],{"all-posts":3,"post:\u002Fithome-2026-day-26":1210},[4,23,35,46,57,68,80,91,102,114,126,138,149,160,171,182,193,204,215,226,237,248,258,268,278,288,298,308,319,330,341,354,368,380,390,400,411,421,431,441,451,461,471,481,491,501,511,521,531,541,551,561,571,581,591,601,611,621,631,641,651,661,675,691,704,719,731,742,752,762,772,782,792,802,812,822,832,842,852,862,871,881,891,901,911,921,931,941,951,961,971,980,990,1000,1010,1020,1036,1050,1062,1073,1083,1095,1107,1121,1134,1145,1156,1167,1180,1191,1201],{"path":5,"title":6,"description":7,"date":8,"updated":9,"tags":10,"categories":14,"draft":15,"wordCount":16,"readingTime":17,"canonical":18,"series":19},"\u002Fithome-2026-day-30","Day 30：Angular Router Providers 與生命週期管理，路由離開後 Service 會銷毀嗎？","平常在 Angular 中，我們可以透過 providers 決定 Service 的注入範圍。例如註冊在 Root，讓整個 Application 共用同一個實例；或註冊在 Component，讓 Service 隨著…","2026-10-14T14:46:39+08:00",null,[11,12,13],"iThome 鐵人賽","2026 鐵人賽","Angular",[11,13],false,7227,7,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10424222",{"name":20,"url":21,"day":22},"2026 iThome 鐵人賽：Angular 22 Signal 進化論","https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F9939",30,{"path":24,"title":25,"description":26,"date":27,"updated":9,"tags":28,"categories":29,"draft":15,"wordCount":30,"readingTime":31,"canonical":32,"series":33},"\u002Fithome-2026-day-29","Day 29：Angular 也有 VueUse？使用 Signality 整合常見 Browser API","在 Vue 生態系中，VueUse 提供許多 Composables，讓我們可以快速使用瀏覽器功能，例如監聽網路狀態、保存使用者設定，或偵測元素是否進入可視範圍。","2026-10-13T23:14:18+08:00",[11,12,13],[11,13],6387,6,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10424070",{"name":20,"url":21,"day":34},29,{"path":36,"title":37,"description":38,"date":39,"updated":9,"tags":40,"categories":41,"draft":15,"wordCount":42,"readingTime":31,"canonical":43,"series":44},"\u002Fithome-2026-day-28","Day 28：Angular 動態建立 Component，NgComponentOutlet 與 ViewContainerRef","平常在 Angular 中使用 Component，通常會直接在 Template 裡面宣告：","2026-10-12T17:27:41+08:00",[11,12,13],[11,13],6565,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423641",{"name":20,"url":21,"day":45},28,{"path":47,"title":48,"description":49,"date":50,"updated":9,"tags":51,"categories":52,"draft":15,"wordCount":53,"readingTime":17,"canonical":54,"series":55},"\u002Fithome-2026-day-27","Day 27：NgRx Store vs SignalStore，從 Action、Reducer 到直接更新 State","前幾天我們使用 NgRx SignalStore 實作購物車，透過 withState() 管理商品資料、withComputed() 計算總金額，再利用 withMethods() 與 patchState() 更新 …","2026-10-11T20:06:41+08:00",[11,12,13],[11,13],7308,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423362",{"name":20,"url":21,"day":56},27,{"path":58,"title":59,"description":60,"date":61,"updated":9,"tags":62,"categories":63,"draft":15,"wordCount":64,"readingTime":17,"canonical":65,"series":66},"\u002Fithome-2026-day-26","Day 26：SignalStore 與 Strategy Pattern，透過 DI 動態切換業務邏輯","昨天我們使用 Angular 原生 Signal 與 NgRx SignalStore 實作購物車，透過 State 管理商品資料，再利用 Computed 計算商品數量與總金額。","2026-10-10T22:01:59+08:00",[11,12,13],[11,13],7347,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423116",{"name":20,"url":21,"day":67},26,{"path":69,"title":70,"description":71,"date":72,"updated":9,"tags":73,"categories":74,"draft":15,"wordCount":75,"readingTime":76,"canonical":77,"series":78},"\u002Fithome-2026-day-25","Day 25：Angular SignalStore，原生 Signal 不夠用嗎？","在 Angular 開發中，如果資料只會被單一 Component 使用，通常可以直接透過 signal() 管理。但當多個 Component 需要共用同一份資料時，就得考慮狀態應該放在哪裡。","2026-10-09T21:28:45+08:00",[11,12,13],[11,13],9117,9,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422784",{"name":20,"url":21,"day":79},25,{"path":81,"title":82,"description":83,"date":84,"updated":9,"tags":85,"categories":86,"draft":15,"wordCount":87,"readingTime":31,"canonical":88,"series":89},"\u002Fithome-2026-day-24","Day 24：SPA Router 為什麼不再只靠 History API？","平常使用 Angular Router 時，可以透過 routerLink 或 router.navigate() 切換不同頁面。","2026-10-08T14:30:47+08:00",[11,12,13],[11,13],6330,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422348",{"name":20,"url":21,"day":90},24,{"path":92,"title":93,"description":94,"date":95,"updated":9,"tags":96,"categories":97,"draft":15,"wordCount":98,"readingTime":17,"canonical":99,"series":100},"\u002Fithome-2026-day-23","Day 23：RouteReuseStrategy，切換路由一定要銷毀 Component 嗎？","在後台系統中，很常會看到頁籤式介面，例如：","2026-10-07T15:58:32+08:00",[11,12,13],[11,13],7361,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422016",{"name":20,"url":21,"day":101},23,{"path":103,"title":104,"description":105,"date":106,"updated":9,"tags":107,"categories":108,"draft":15,"wordCount":109,"readingTime":110,"canonical":111,"series":112},"\u002Fithome-2026-day-22","Day 22：Angular Render Lifecycle，從 afterNextRender 到 DestroyRef","過去在 Angular 中，只要遇到初始化、View 建立或資源清理，通常會先想到 Lifecycle Hooks：","2026-10-06T23:36:23+08:00",[11,12,13],[11,13],8111,8,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10421810",{"name":20,"url":21,"day":113},22,{"path":115,"title":116,"description":117,"date":118,"updated":9,"tags":119,"categories":120,"draft":15,"wordCount":121,"readingTime":122,"canonical":123,"series":124},"\u002Fithome-2026-day-21","Day 21：Angular Template 還適合呼叫 Method 嗎？從 Pipe、RxJS 到 computed","過去在寫 Angular 時，因為 Template 可以直接呼叫 Method，很容易在專案裡看到大量這樣的寫法。","2026-10-05T23:30:32+08:00",[11,12,13],[11,13],5431,5,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10421425",{"name":20,"url":21,"day":125},21,{"path":127,"title":128,"description":129,"date":130,"updated":9,"tags":131,"categories":132,"draft":15,"wordCount":133,"readingTime":134,"canonical":135,"series":136},"\u002Fithome-2026-day-20","Day 20：Signal Forms 非同步驗證，validateHttp 與 validateAsync","前兩篇介紹了 Signal Forms 的基本驗證，以及需要參考其他欄位的 Cross-field Validation。","2026-10-04T20:56:17+08:00",[11,12,13],[11,13],4278,4,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10420910",{"name":20,"url":21,"day":137},20,{"path":139,"title":140,"description":141,"date":142,"updated":9,"tags":143,"categories":144,"draft":15,"wordCount":145,"readingTime":134,"canonical":146,"series":147},"\u002Fithome-2026-day-19","Day 19：Signal Forms Cross-field Validation 與欄位更新時機","上一篇介紹 Signal Forms 時，已經看到可以透過 Schema 定義欄位驗證，例如 required()、email()、minLength()。","2026-10-03T23:18:26+08:00",[11,12,13],[11,13],3783,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10420631",{"name":20,"url":21,"day":148},19,{"path":150,"title":151,"description":152,"date":153,"updated":9,"tags":154,"categories":155,"draft":15,"wordCount":156,"readingTime":31,"canonical":157,"series":158},"\u002Fithome-2026-day-18","Day 18：Angular Signal Forms 穩定了，跟我們用了這麼久的 Reactive Forms 到底差在哪？","在 Angular 中處理表單，很多專案使用的都是 Reactive Forms。","2026-10-02T23:28:36+08:00",[11,12,13],[11,13],5886,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10420249",{"name":20,"url":21,"day":159},18,{"path":161,"title":162,"description":163,"date":164,"updated":9,"tags":165,"categories":166,"draft":15,"wordCount":167,"readingTime":31,"canonical":168,"series":169},"\u002Fithome-2026-day-17","Day 17：effect() 不是拿來同步 Signal 的，Angular 的 Side Effect 該怎麼寫？","effect() 是 Angular signal 裡很容易被誤用的一個 API。因為寫法直覺，只要讀取 signal，就能在值改變時執行一些事情，所以很常被拿來同步狀態、打 API，甚至再去修改其他 signal。","2026-10-01T21:22:13+08:00",[11,12,13],[11,13],6473,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10419779",{"name":20,"url":21,"day":170},17,{"path":172,"title":173,"description":174,"date":175,"updated":9,"tags":176,"categories":177,"draft":15,"wordCount":178,"readingTime":31,"canonical":179,"series":180},"\u002Fithome-2026-day-16","Day 16：即將登場的 Route Resources，Angular Router 為什麼需要新的資料載入方式？","過去在 Angular Router 中，如果希望進入 Component 前先完成資料載入，可以透過 Resolver，讓資料取得流程在 Component 初始化之前執行。","2026-09-30T23:12:05+08:00",[11,12,13],[11,13],6534,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10419409",{"name":20,"url":21,"day":181},16,{"path":183,"title":184,"description":185,"date":186,"updated":9,"tags":187,"categories":188,"draft":15,"wordCount":189,"readingTime":17,"canonical":190,"series":191},"\u002Fithome-2026-day-15","Day 15：搞懂 Injection Context，從 inject()、runInInjectionContext() 到 injectAsync()","前面提到 Injection Context 時，可能會讓人聯想到先前介紹過的 Reactive Context。我一開始也有點搞混，因此花了一些時間重新理解 Angular 裡這些不同的執行環境。","2026-09-29T13:44:28+08:00",[11,12,13],[11,13],6986,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10418752",{"name":20,"url":21,"day":192},15,{"path":194,"title":195,"description":196,"date":197,"updated":9,"tags":198,"categories":199,"draft":15,"wordCount":200,"readingTime":17,"canonical":201,"series":202},"\u002Fithome-2026-day-14","Day 14：Lazy Loading 不只一種，從 Router Preloading、@defer 到 injectAsync()","當 Client Side Rendering 的應用越來越大時，我們通常會開始思考，能不能把原本一次載入的內容拆得更小，等到真正需要時再下載，避免初始載入過多 JavaScript，拉長畫面呈現的時間。","2026-09-28T22:44:46+08:00",[11,12,13],[11,13],7674,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10418485",{"name":20,"url":21,"day":203},14,{"path":205,"title":206,"description":207,"date":208,"updated":9,"tags":209,"categories":210,"draft":15,"wordCount":211,"readingTime":110,"canonical":212,"series":213},"\u002Fithome-2026-day-13","Day 13：從 Router Event 到 Signal","路由狀態管理在前端開發中比較少被單獨拿出來討論，但在 Angular 裡，一次路由導航其實會經過不少階段，也會在過程中發出不同的 Router Event，例如 NavigationStart、NavigationEnd…","2026-09-27T20:37:55+08:00",[11,12,13],[11,13],7777,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10417948",{"name":20,"url":21,"day":214},13,{"path":216,"title":217,"description":218,"date":219,"updated":9,"tags":220,"categories":221,"draft":15,"wordCount":222,"readingTime":122,"canonical":223,"series":224},"\u002Fithome-2026-day-12","Day 12：從 httpResource 到 TanStack Query，認識 Server State","前面我們花了不少時間討論，為什麼「請求本身的狀態」和「元件的顯示狀態」應該分開處理。","2026-09-26T23:35:12+08:00",[11,12,13],[11,13],5119,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10417614",{"name":20,"url":21,"day":225},12,{"path":227,"title":228,"description":229,"date":230,"updated":9,"tags":231,"categories":232,"draft":15,"wordCount":233,"readingTime":122,"canonical":234,"series":235},"\u002Fithome-2026-day-11","Day 11：用 rxResource 串起 Observable 與 Resource","前面介紹了不少 Resource 到 httpResource() 的應用。不過在 Angular 開發中，Observable 與 RxJS 仍然是很常見的非同步資料處理方式，像是 switchMap、map、debo…","2026-09-25T22:42:04+08:00",[11,12,13],[11,13],4908,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10417143",{"name":20,"url":21,"day":236},11,{"path":238,"title":239,"description":240,"date":241,"updated":9,"tags":242,"categories":243,"draft":15,"wordCount":244,"readingTime":31,"canonical":245,"series":246},"\u002Fithome-2026-day-10","Day 10：linkedSignal 讓相依狀態也能被修改","昨天我們透過 httpResource() 搭配 chain() 處理兩個非同步資料之間的依賴。下游 Request 需要使用上游資料時，會等上游完成後再繼續；當 Request 的來源改變，Resource 的狀態也會…","2026-09-24T23:49:49+08:00",[11,12,13],[11,13],5623,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10416746",{"name":20,"url":21,"day":247},10,{"path":249,"title":250,"description":251,"date":252,"updated":9,"tags":253,"categories":254,"draft":15,"wordCount":255,"readingTime":134,"canonical":256,"series":257},"\u002Fithome-2026-day-09","Day 9：httpResource 實務篇，統一回覆格式與 chain 相依請求","昨天我們透過 Orval 串起 API Contract，了解如何根據 OpenAPI 規格產生前端需要的型別與 API 呼叫方式。","2026-09-23T23:42:40+08:00",[11,12,13],[11,13],4358,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10416227",{"name":20,"url":21,"day":76},{"path":259,"title":260,"description":261,"date":262,"updated":9,"tags":263,"categories":264,"draft":15,"wordCount":265,"readingTime":122,"canonical":266,"series":267},"\u002Fithome-2026-day-08","Day 8：用 Orval 串起 Angular API Contract，同時產生 HttpClient、httpResource 與 Zod","前後端在對接 API 時，常見的問題之一，就是雙方對資料格式的定義逐漸不一致。","2026-09-22T23:48:12+08:00",[11,12,13],[11,13],4451,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10415694",{"name":20,"url":21,"day":110},{"path":269,"title":270,"description":271,"date":272,"updated":9,"tags":273,"categories":274,"draft":15,"wordCount":275,"readingTime":17,"canonical":276,"series":277},"\u002Fithome-2026-day-07","Day 7：從 Resource 到 httpResource，用 Signal 管理 HTTP 資料請求","昨天我們提到，Resource API 本身是一套通用的非同步資料模型，比較適合用在「讀取資料」這類情境，而不是新增、修改或刪除資料這種 Mutation 操作。","2026-09-21T22:13:18+08:00",[11,12,13],[11,13],6735,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10415038",{"name":20,"url":21,"day":17},{"path":279,"title":280,"description":281,"date":282,"updated":9,"tags":283,"categories":284,"draft":15,"wordCount":285,"readingTime":122,"canonical":286,"series":287},"\u002Fithome-2026-day-06","Day 6：Resource API，把非同步資料帶進 Signal","今天來看 Angular Signal 裡一個很重要的非同步資料 API：Resource API。","2026-09-20T19:25:37+08:00",[11,12,13],[11,13],5054,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10414377",{"name":20,"url":21,"day":31},{"path":289,"title":290,"description":291,"date":292,"updated":9,"tags":293,"categories":294,"draft":15,"wordCount":295,"readingTime":134,"canonical":296,"series":297},"\u002Fithome-2026-day-05","Day 5：從 Reactive Graph 到 View Tree，看 Angular 如何找到真正需要更新的畫面","前面我們已經知道，當 Signal 的值發生變化後，Angular 會透過 Reactive Graph 找到受到影響的 Consumer。","2026-09-19T20:19:47+08:00",[11,12,13],[11,13],4364,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10413852",{"name":20,"url":21,"day":122},{"path":299,"title":300,"description":301,"date":302,"updated":9,"tags":303,"categories":304,"draft":15,"wordCount":305,"readingTime":122,"canonical":306,"series":307},"\u002Fithome-2026-day-04","Day 4：Angular 怎麼知道誰正在讀 Signal？從 Reactive Context 理解依賴追蹤","前面我們看過，當 Template 讀取 Signal 時，Angular 會記住「誰依賴了誰」；當 Signal 發生變化後，也會透過 Reactive Graph 通知相關的 Consumer。","2026-09-18T21:58:16+08:00",[11,12,13],[11,13],4894,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10413313",{"name":20,"url":21,"day":134},{"path":309,"title":310,"description":311,"date":312,"updated":9,"tags":313,"categories":314,"draft":15,"wordCount":315,"readingTime":122,"canonical":316,"series":317},"\u002Fithome-2026-day-03","Day 3：從 set()、update() 看懂 Angular Signal 的基本使用與更新機制","昨天我們先試著理解，在 Template 中使用 Signal() 時，Angular 是如何建立依賴關係，並在 Signal 發生變化後，知道哪些 View 可能受到影響。","2026-09-17T23:08:58+08:00",[11,12,13],[11,13],5451,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10412826",{"name":20,"url":21,"day":318},3,{"path":320,"title":321,"description":322,"date":323,"updated":9,"tags":324,"categories":325,"draft":15,"wordCount":326,"readingTime":134,"canonical":327,"series":328},"\u002Fithome-2026-day-02","Day 2：Angular Signal 是什麼？從 Change Detection 理解它的運作原理","傳統 Angular 在監測資料變化時，通常會搭配 zone.js。像是 DOM 事件、Promise、Timer、HTTP 請求這些非同步操作發生時，zone.js 會幫忙攔截。","2026-09-16T20:19:12+08:00",[11,12,13],[11,13],4276,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10411944",{"name":20,"url":21,"day":329},2,{"path":331,"title":332,"description":333,"date":334,"updated":9,"tags":335,"categories":336,"draft":15,"wordCount":337,"readingTime":329,"canonical":338,"series":339},"\u002Fithome-2026-day-01","Day 1：為什麼想聊 Angular Signal？","相信講到前端框架，大家第一時間想到的通常會是 Vue、React，甚至也有人會推薦 Svelte 這類近年討論度很高的框架。但一講到 Angular，很多人的第一印象可能就像看到一座大城堡一樣：東西很多、規則很多，一時之…","2026-09-15T23:56:26+08:00",[11,12,13],[11,13],1478,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10411425",{"name":20,"url":21,"day":340},1,{"path":342,"title":343,"description":344,"date":345,"updated":9,"tags":346,"categories":351,"draft":15,"wordCount":353,"readingTime":134,"canonical":9,"series":9},"\u002FTS-Excess-Property-Checks","TypeScript 踩坑筆記：物件字面量的多餘屬性檢查機制","深入探討 TypeScript 的多餘屬性檢查（Excess Property Checks）機制，解析物件字面量與變數賦值的型別檢查差異，並提供實務開發中避免型別陷阱的最佳實踐方法。","2026-01-31T16:29:23+08:00",[347,348,349,350],"Excess Property Checks","Structural Typing","Type Safety","NestJS",[352],"TypeScript",3551,{"path":355,"title":356,"description":357,"date":358,"updated":9,"tags":359,"categories":361,"draft":15,"wordCount":363,"readingTime":329,"canonical":364,"series":365},"\u002Fithome-2025-day-30","Day 30：開發之旅完結 - 感謝與展望","終於來到鐵人賽的最後一天！這是我第二次參加鐵人賽，今年的主題是 LINE Bot 開發。這個主題源自於我參加的十二週讀書會分享，當時我發現大家對 LINE Bot 很感興趣，而我自己也一直很喜歡這個主題，於是早在六月就定…","2025-10-13T20:11:12+08:00",[11,360],"2025 鐵人賽",[11,362],"Line Bot",1751,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396850",{"name":366,"url":367,"day":22},"2025 iThome 鐵人賽：Line Bot × NestJS：30 天開發日記","https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F8779",{"path":369,"title":370,"description":371,"date":372,"updated":9,"tags":373,"categories":376,"draft":15,"wordCount":377,"readingTime":31,"canonical":378,"series":379},"\u002Fithome-2025-day-29","Day 29：LIFF 會員註冊 - 信箱驗證與 Swagger 文件整合","延續 Day 28 會員註冊的流程，在實際的會員註冊場景中，電子信箱驗證是確保用戶身份真實性的重要環節。本篇將基於昨天的註冊流程，新增兩個核心 API：發送驗證信與驗證驗證碼，讓會員註冊功能更加完整且安全。","2025-10-12T23:15:00+08:00",[11,360,362,374,350,375],"LIFF","Swagger",[11,362],6089,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396512",{"name":366,"url":367,"day":34},{"path":381,"title":382,"description":383,"date":384,"updated":9,"tags":385,"categories":386,"draft":15,"wordCount":387,"readingTime":122,"canonical":388,"series":389},"\u002Fithome-2025-day-28","Day 28：LIFF 會員註冊 - ID Token 驗證與資料庫整合","今天的開發重點是建立會員卡系統，將 LIFF 前端與 NestJS 後端完整串接。","2025-10-11T22:57:29+08:00",[11,360,362,374,350],[11,362],5254,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396146",{"name":366,"url":367,"day":45},{"path":391,"title":392,"description":393,"date":394,"updated":9,"tags":395,"categories":396,"draft":15,"wordCount":397,"readingTime":318,"canonical":398,"series":399},"\u002Fithome-2025-day-27","Day 27：群組事件與檔案訊息接收","今天主要補充 LINE Bot 在群組中的專屬特性，包括群組限定的 Webhook 事件，以及前面提到但尚未說明的第 7 種訊息類型 file 檔案訊息。","2025-10-10T21:01:52+08:00",[11,360,362,350],[11,362],2888,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10395785",{"name":366,"url":367,"day":56},{"path":401,"title":402,"description":403,"date":404,"updated":9,"tags":405,"categories":407,"draft":15,"wordCount":408,"readingTime":122,"canonical":409,"series":410},"\u002Fithome-2025-day-26","Day 26：LINE Pay 線上付款","LINE Pay 提供兩種付款方式：線上付款與實體付款。實體付款主要採用店家出示條碼的模式，顧客掃描後即可輸入金額完成付款。這兩種付款方式都支援在沙盒(Sandbox)環境中進行測試。本文將著重說明線上付款的整合流程。","2025-10-09T23:39:32+08:00",[11,360,406],"Line Pay",[11,362],4876,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10395481",{"name":366,"url":367,"day":67},{"path":412,"title":413,"description":414,"date":415,"updated":9,"tags":416,"categories":417,"draft":15,"wordCount":418,"readingTime":318,"canonical":419,"series":420},"\u002Fithome-2025-day-25","Day 25：優惠券設定與 Coupon Message 實作","LINE Official Account 的「優惠券」是提升顧客互動與忠誠度的重要功能。透過獎勵機制，不僅能吸引新客，更能有效促進回購。","2025-10-08T23:49:32+08:00",[11,360,362,350],[11,362],3207,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10395076",{"name":366,"url":367,"day":79},{"path":422,"title":423,"description":424,"date":425,"updated":9,"tags":426,"categories":427,"draft":15,"wordCount":428,"readingTime":134,"canonical":429,"series":430},"\u002Fithome-2025-day-24","Day 24：LIFF 刮刮樂實戰體驗","在了解 LIFF 的基礎設計流程後，本文將透過「刮刮樂」案例示範 LIFF API 的實際應用。從啟動時檢查權限與執行環境、確認用戶分享功能權限，到刮刮樂結束後發送訊息並關閉 LIFF，完整呈現 LIFF 在互動應用中的…","2025-10-07T22:36:58+08:00",[11,360,362,374],[11,362],4200,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10394618",{"name":366,"url":367,"day":90},{"path":432,"title":433,"description":434,"date":435,"updated":9,"tags":436,"categories":437,"draft":15,"wordCount":438,"readingTime":134,"canonical":439,"series":440},"\u002Fithome-2025-day-23","Day 23：LIFF 網頁應用開發入門","在鐵人賽前半段，我們深入探討了 LINE Bot 的兩大核心功能：Richmenu(選單設置)與各種 Reply Message(回覆訊息)的訊息類型。然而在實務應用中，若要提供更豐富的互動體驗，Line 往往需要搭配 …","2025-10-06T23:06:57+08:00",[11,360,362,374],[11,362],4400,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10394035",{"name":366,"url":367,"day":101},{"path":442,"title":443,"description":444,"date":445,"updated":9,"tags":446,"categories":447,"draft":15,"wordCount":448,"readingTime":329,"canonical":449,"series":450},"\u002Fithome-2025-day-22","Day 22：Render 雲端平台部署 Nest 後端伺服器","今天的目標是將系統正式上線。經過這段時間的開發，我們已完成 Line Message API 的核心功能與日誌處理機制。","2025-10-05T22:52:11+08:00",[11,360,362,350],[11,362],1895,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10393809",{"name":366,"url":367,"day":113},{"path":452,"title":453,"description":454,"date":455,"updated":9,"tags":456,"categories":457,"draft":15,"wordCount":458,"readingTime":318,"canonical":459,"series":460},"\u002Fithome-2025-day-21","Day 21：Nest Pino 整合 Grafana Loki 日誌","Loki 環境變數可參考 Day 20 文章的 Grafana Data Source settings 設定","2025-10-04T17:24:26+08:00",[11,360,362,350],[11,362],2740,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10393305",{"name":366,"url":367,"day":125},{"path":462,"title":463,"description":464,"date":465,"updated":9,"tags":466,"categories":467,"draft":15,"wordCount":468,"readingTime":318,"canonical":469,"series":470},"\u002Fithome-2025-day-20","Day 20：Grafana Loki 日誌服務 - 從申請到 API 串接測試","目前已經理解了 LINE Messaging API 的核心功能，包括如何接收訊息、串接第三方服務，以及發送多種類型的訊息。接下來，我們將把原本架設在 ngrok 的後端伺服器，透過 GitHub 自動化部署到 Rend…","2025-10-03T23:16:16+08:00",[11,360,362,350],[11,362],2461,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10392910",{"name":366,"url":367,"day":137},{"path":472,"title":473,"description":474,"date":475,"updated":9,"tags":476,"categories":477,"draft":15,"wordCount":478,"readingTime":318,"canonical":479,"series":480},"\u002Fithome-2025-day-19","Day 19：Flex Message Carousel 食譜分享","Flex Message 可以分成 bubble(單張卡片) 跟 carousel(多張卡片輪播) 兩種方式，昨天示範了 bubble 格式的電子個人名片後，今天主要展示 carousel 的應用，兩者的基本結構大致相同…","2025-10-02T16:06:17+08:00",[11,360,362,350],[11,362],2267,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10392256",{"name":366,"url":367,"day":148},{"path":482,"title":483,"description":484,"date":485,"updated":9,"tags":486,"categories":487,"draft":15,"wordCount":488,"readingTime":122,"canonical":489,"series":490},"\u002Fithome-2025-day-18","Day 18：Flex Message 設計個人名片","從基礎的文字互動開始，我們逐步探索了六大核心訊息類型的發送機制、Template Message 的結構化設計，以及 ImageMap Message 的互動式體驗。今天，我們將進入 LINE Bot 最具彈性的訊息格式…","2025-10-01T17:57:47+08:00",[11,360,362,350],[11,362],4468,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10391723",{"name":366,"url":367,"day":159},{"path":492,"title":493,"description":494,"date":495,"updated":9,"tags":496,"categories":497,"draft":15,"wordCount":498,"readingTime":122,"canonical":499,"series":500},"\u002Fithome-2025-day-17","Day 17：ImageMap Message  從互動海報到影片嵌入","在 Day 16 文章中，我們使用 Cloudflare Workers 實作了圖片代理功能。到目前為止，我們已經完成以下準備工作:","2025-09-30T11:58:32+08:00",[11,360,362,350],[11,362],4807,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10391164",{"name":366,"url":367,"day":170},{"path":502,"title":503,"description":504,"date":505,"updated":9,"tags":506,"categories":507,"draft":15,"wordCount":508,"readingTime":134,"canonical":509,"series":510},"\u002Fithome-2025-day-16","Day 16：Cloudflare Workers 圖片代理實作","在 Day 15 文章中，我們處理了 ImageMap Message 的背景圖，接著我們遇到了圖片不能出現副檔名及動態路徑支援不同解析度的問題。今天主要會透過 Cloudflare Workers 來解決這兩個問題，讓…","2025-09-29T16:15:41+08:00",[11,360,362,350],[11,362],3412,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10390567",{"name":366,"url":367,"day":181},{"path":512,"title":513,"description":514,"date":515,"updated":9,"tags":516,"categories":517,"draft":15,"wordCount":518,"readingTime":134,"canonical":519,"series":520},"\u002Fithome-2025-day-15","Day 15：共通屬性處理優化及 Imagemap  圖片製作","針對 Day 14 的程式碼架構進行優化。由於 Sender 和 Quick Reply 功能在所有回覆訊息類型中都能使用，我們應該將這些共用屬性抽取為獨立函數進行統一處理。","2025-09-28T21:03:38+08:00",[11,360,362,350],[11,362],4033,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10389697",{"name":366,"url":367,"day":192},{"path":522,"title":523,"description":524,"date":525,"updated":9,"tags":526,"categories":527,"draft":15,"wordCount":528,"readingTime":31,"canonical":529,"series":530},"\u002Fithome-2025-day-14","Day 14：LINE Bot Template Message 訊息回覆","經過前幾天的學習，我們已經成功掌握了 LINE Bot 六大基礎訊息類型的發送技巧，為訊息互動奠定了穩固的基礎。今天，我們將踏入更具挑戰性的進階應用領域！","2025-09-27T15:54:06+08:00",[11,360,362,350],[11,362],6372,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10388976",{"name":366,"url":367,"day":203},{"path":532,"title":533,"description":534,"date":535,"updated":9,"tags":536,"categories":537,"draft":15,"wordCount":538,"readingTime":122,"canonical":539,"series":540},"\u002Fithome-2025-day-13","Day 13：LINE Bot 客製化 Sender 與 Quick Reply 功能實作","LINE Bot 所有回覆訊息類型都具備「共用屬性」功能，這些屬性帶來兩項重要的互動體驗提升：","2025-09-26T11:41:34+08:00",[11,360,362,350],[11,362],4426,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10388037",{"name":366,"url":367,"day":214},{"path":542,"title":543,"description":544,"date":545,"updated":9,"tags":546,"categories":547,"draft":15,"wordCount":548,"readingTime":318,"canonical":549,"series":550},"\u002Fithome-2025-day-12","Day 12：LINE Bot Image、Video、Audio、Location 訊息回覆","Day 11 文章中，我們針對 LINE Bot 可以回覆的Text及Sticker基礎型別進行了封裝，不僅新增了 line-message.ts模組，還讓開發者可以用最少的參數產生 LINE Message 所需要的格…","2025-09-25T18:08:09+08:00",[11,360,362,350],[11,362],2977,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10387598",{"name":366,"url":367,"day":225},{"path":552,"title":553,"description":554,"date":555,"updated":9,"tags":556,"categories":557,"draft":15,"wordCount":558,"readingTime":31,"canonical":559,"series":560},"\u002Fithome-2025-day-11","Day 11：LINE Bot Text Emoji 與 Sticker 訊息回覆","目前我們的 LINE Bot 主要使用 Text 方式回覆訊息。今天將探討 Text 類型的 Emoji 應用，以及 Sticker 回覆方式，基於 Day 8 版本進行改寫。","2025-09-24T23:21:11+08:00",[11,360,350,362],[11,362],5659,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10386993",{"name":366,"url":367,"day":236},{"path":562,"title":563,"description":564,"date":565,"updated":9,"tags":566,"categories":567,"draft":15,"wordCount":568,"readingTime":318,"canonical":569,"series":570},"\u002Fithome-2025-day-10","Day 10：LINE Rich Menu Switch Action 提升使用者體驗","在 Day 9 文章中，我們結合 LINE 平台的功能，將天氣服務與圖片上傳服務整合，打造出第一張 LINE 圖文選單，透過 URI action 的方式觸發平台提供的互動效果。接著，我們使用客製化選單製作出 9 格的客…","2025-09-23T15:02:57+08:00",[11,360,362,350],[11,362],2906,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10386165",{"name":366,"url":367,"day":247},{"path":572,"title":573,"description":574,"date":575,"updated":9,"tags":576,"categories":577,"draft":15,"wordCount":578,"readingTime":318,"canonical":579,"series":580},"\u002Fithome-2025-day-09","Day  9：打造 LINE Rich Menu - 後台設計 vs API 客製化","在 Day8 文章中，我們透過整合雲端服務 Cloudinary，學習如何處理使用者上傳的圖片檔案，並將其儲存至雲端。然而，LINE Bot 除了讓使用者輸入文字回覆的方式之外，更常見的是透過圖文選單(Rich Menu…","2025-09-22T21:54:25+08:00",[11,360,362,350],[11,362],3285,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10385658",{"name":366,"url":367,"day":76},{"path":582,"title":583,"description":584,"date":585,"updated":9,"tags":586,"categories":587,"draft":15,"wordCount":588,"readingTime":17,"canonical":589,"series":590},"\u002Fithome-2025-day-08","Day 8：Cloudinary 雲端圖片儲存整合","在 Day 6 中，我們透過整合 OpenWeatherMap API 與座標位置訊息學習了如何結合第三方服務。今天將探索 Cloudinary 雲端服務，解決 LINE 圖片資源會過期的問題，學習如何透過 LINE M…","2025-09-21T23:13:19+08:00",[11,360,362,350],[11,362],7215,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10384968",{"name":366,"url":367,"day":110},{"path":592,"title":593,"description":594,"date":595,"updated":9,"tags":596,"categories":597,"draft":15,"wordCount":598,"readingTime":122,"canonical":599,"series":600},"\u002Fithome-2025-day-07","Day 7：Joi 環境變數統一驗證，重構 LINE Bot 天氣服務模組","在 Day 6 開發 LINE Bot 結合 OpenWeatherMap API 的過程中，我們採用私有方法將 API 請求處理與資料格式化整合在一起。今天將對 Day 6 的實作進行架構重構，主要目標是建立更完善的環…","2025-09-20T23:07:08+08:00",[11,360,362,350],[11,362],5157,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10384302",{"name":366,"url":367,"day":17},{"path":602,"title":603,"description":604,"date":605,"updated":9,"tags":606,"categories":607,"draft":15,"wordCount":608,"readingTime":134,"canonical":609,"series":610},"\u002Fithome-2025-day-06","Day 6：整合第三方 API 天氣查詢服務","Day 5 帶大家了解 LINE Bot 能接收使用者傳遞的「6+1」種訊息類型。之所以這樣強調，是因為 File 類型僅能在群組聊天中使用，這個限制我自己也經常忘記，所以習慣用「6+1」來提醒自己。","2025-09-19T21:07:47+08:00",[11,360,362,350],[11,362],3892,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10383636",{"name":366,"url":367,"day":31},{"path":612,"title":613,"description":614,"date":615,"updated":9,"tags":616,"categories":617,"draft":15,"wordCount":618,"readingTime":122,"canonical":619,"series":620},"\u002Fithome-2025-day-05","Day 5：整合 Pino Logger 提升 LINE Bot 專案的可維護性","看著專案逐漸成型，現在該開始重視日誌記錄的規劃了。在實際部署時，我們通常會區分 develop(開發環境)和 production(正式環境)兩種不同的執行環境，針對不同環境採用相應的日誌策略。","2025-09-18T23:30:00+08:00",[11,360,362,350],[11,362],4746,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10382298",{"name":366,"url":367,"day":122},{"path":622,"title":623,"description":624,"date":625,"updated":9,"tags":626,"categories":627,"draft":15,"wordCount":628,"readingTime":31,"canonical":629,"series":630},"\u002Fithome-2025-day-04","Day 4：掌握 LINE Bot 七種訊息類型與 NestJS 架構優化","目前我們都只使用 LINE Message Event 中的純文字(text)格式進行回覆，但除了純文字之外，LINE Bot 後端伺服器其實可以接收多種不同類型的訊息。今天就讓我們一起來深入了解這些訊息類型吧！","2025-09-17T22:53:37+08:00",[11,360,362,350],[11,362],5828,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10381651",{"name":366,"url":367,"day":134},{"path":632,"title":633,"description":634,"date":635,"updated":9,"tags":636,"categories":637,"draft":15,"wordCount":638,"readingTime":122,"canonical":639,"series":640},"\u002Fithome-2025-day-03","Day 3：Webhook 簽章驗證與訊息回覆策略(Push & Reply)","上一篇帶大家透過「Webhook 模式」成功架設了專屬的 LINE Bot 代理人。相信大家在體驗基礎功能後，對於 LINE Bot Webhook 還能監聽哪些事件、支援哪些類型的訊息回覆，以及如何確保後端收到的訊息確…","2025-09-16T21:53:39+08:00",[11,360,362,350],[11,362],5360,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10381303",{"name":366,"url":367,"day":318},{"path":642,"title":643,"description":644,"date":645,"updated":9,"tags":646,"categories":647,"draft":15,"wordCount":648,"readingTime":31,"canonical":649,"series":650},"\u002Fithome-2025-day-02","Day 2：從 Webhook 到後端，打造你的專屬機器人","上一篇，帶大家透過「Chat 模式」，讓自身變成客服機器人的方式。這篇會從「Webhook 模式」切入，並以 NestJS 作為後端框架，教你如何在休息時間，也能讓後端伺服器當你的替身幫你解惑使用者的問題。","2025-09-15T21:20:49+08:00",[11,360,362,350],[11,362],6590,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10380524",{"name":366,"url":367,"day":329},{"path":652,"title":653,"description":654,"date":655,"updated":9,"tags":656,"categories":657,"draft":15,"wordCount":658,"readingTime":318,"canonical":659,"series":660},"\u002Fithome-2025-day-01","Day 1：Hello World LINE Bot","每個程式語言的開始通常都會以練習 Hello World！作為一個簡單的起手式，LINE Bot 也不意外。這一系列會著重在 LINE Bot 的相關應用，帶大家用輕鬆的方式一起建立屬於自己的 Line Bot 聊天機器…","2025-09-14T22:48:33+08:00",[11,360,362,350],[11,362],2723,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10379895",{"name":366,"url":367,"day":340},{"path":662,"title":663,"description":664,"date":665,"updated":9,"tags":666,"categories":671,"draft":15,"wordCount":674,"readingTime":134,"canonical":9,"series":9},"\u002Fdbdiagram-tool","關聯式資料庫設計必備 - dbdiagram.io ERD 繪圖工具","還在煩惱如何設計關聯式資料表實體關聯圖嗎？ 使用 dbdiagram.io 加快你與同事之間的距離","2024-11-05T20:18:24+08:00",[667,668,669,670],"ERD","dbdiagram.io","DBML","資料庫設計",[672,673],"Database","開發工具",3982,{"path":676,"title":677,"description":678,"date":679,"updated":9,"tags":680,"categories":687,"draft":15,"wordCount":690,"readingTime":122,"canonical":9,"series":9},"\u002FPostgreSQL-1","前端勇士大闖六角後端體驗營 - 初探 PostgreSQL DML 語法","初步踏入後端的世界，了解何謂後端以及資料庫常見語法 DDL、DML。","2024-10-30T22:43:49+08:00",[681,682,683,684,685,686],"PostgreSQL","SQL","DML","DDL","資料庫","六角學院",[672,688,689],"後端實戰","學習心得",5220,{"path":692,"title":693,"description":694,"date":695,"updated":9,"tags":696,"categories":700,"draft":15,"wordCount":703,"readingTime":329,"canonical":9,"series":9},"\u002Fcss-flex-1","CSS flex 軸線對齊技巧 - 預設屬性","CSS flex 排版技巧你都了解了嗎？回歸學習初心的認識 flex 預設屬性","2024-10-24T20:29:08+08:00",[697,698,699],"Flexbox","前端排版","切版技巧",[701,702],"CSS","前端開發",1856,{"path":705,"title":706,"description":707,"date":708,"updated":9,"tags":709,"categories":712,"draft":15,"wordCount":714,"readingTime":329,"canonical":715,"series":716},"\u002Fithome-2024-day-30","Vue 3 用實作帶你看過核心概念 - Day 30：新手村的鐵人證書 - 完賽心得","花了整整半年左右的時間，每天下班都很用心的準備範例或是看書，這一段旅程對我來說真的很重要。擔心、害怕跟不安的心情一直充斥自己心中，擔心寫錯或是寫不好，想讓自己再多準備久一點再開始，所以拖到中秋連假才開始，想下定決心讓自己…","2024-10-13T12:42:25+08:00",[11,710,711],"2024 鐵人賽","Vue 3",[11,713],"Vue",1381,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10367206",{"name":717,"url":718,"day":22},"2024 iThome 鐵人賽：Vue 3 初學者：用實作帶你看過核心概念","https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F7800",{"path":720,"title":721,"description":722,"date":723,"updated":9,"tags":724,"categories":727,"draft":15,"wordCount":728,"readingTime":134,"canonical":729,"series":730},"\u002Fithome-2024-day-29","Vue 3 用實作帶你看過核心概念 - Day 29：Composables(組合式函數)","Composables是 Vue 3 中的一個重要概念，它是利用 Vue 的 Composition API 來創建和管理可重用的有狀態邏輯的函數。Composables 允許我們將組件邏輯抽象成可重用的函數，使得代碼更…","2024-10-12T15:07:55+08:00",[11,710,711,725,726],"Options API","Composition API",[11,713],4233,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10366865",{"name":717,"url":718,"day":34},{"path":732,"title":733,"description":734,"date":735,"updated":9,"tags":736,"categories":738,"draft":15,"wordCount":739,"readingTime":134,"canonical":740,"series":741},"\u002Fithome-2024-day-28","Vue 3 用實作帶你看過核心概念 - Day 28：官方 Vue Router 的應用與技巧","路由設定方式，一般可以分成兩種：","2024-10-11T17:14:44+08:00",[11,710,711,725,737],"Vue Router",[11,713],4095,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10366520",{"name":717,"url":718,"day":45},{"path":743,"title":744,"description":745,"date":746,"updated":9,"tags":747,"categories":748,"draft":15,"wordCount":749,"readingTime":122,"canonical":750,"series":751},"\u002Fithome-2024-day-27","Vue 3 用實作帶你看過核心概念 - Day 27：VeeValidate 表單驗證工具","在開發 Vue 應用程式時，表單驗證至關重要，它能確保使用者輸入的資料符合預期的格式和要求。今天來介紹一款專門用於 Vue 的表單驗證工具—VeeValidate v4。這個工具不僅能幫助開發者輕鬆實現表單驗證，還提供了…","2024-10-10T21:02:16+08:00",[11,710,711,725],[11,713],4461,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10366165",{"name":717,"url":718,"day":56},{"path":753,"title":754,"description":755,"date":756,"updated":9,"tags":757,"categories":758,"draft":15,"wordCount":759,"readingTime":318,"canonical":760,"series":761},"\u002Fithome-2024-day-26","Vue 3 用實作帶你看過核心概念 - Day 26：KeepAlive 與 Teleport 內置組件","預設情況下，切換組件會導致組件的創建及銷毀，無法保持組件內部的狀態。","2024-10-09T18:50:53+08:00",[11,710,711,725],[11,713],3158,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10365724",{"name":717,"url":718,"day":67},{"path":763,"title":764,"description":765,"date":766,"updated":9,"tags":767,"categories":768,"draft":15,"wordCount":769,"readingTime":318,"canonical":770,"series":771},"\u002Fithome-2024-day-25","Vue 3 用實作帶你看過核心概念 - Day 25：自定義指令(Custom Directives)","Vue不僅提供了豐富的內置指令用於操作DOM元素，還允許開發者創建自定義指令。透過自定義指令，我們可以對DOM元素進行更細緻的控制，並在指令的不同生命週期階段執行特定的邏輯，從而實現更靈活的功能擴展。","2024-10-08T22:05:32+08:00",[11,710,711,725],[11,713],3116,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10365355",{"name":717,"url":718,"day":79},{"path":773,"title":774,"description":775,"date":776,"updated":9,"tags":777,"categories":778,"draft":15,"wordCount":779,"readingTime":329,"canonical":780,"series":781},"\u002Fithome-2024-day-24","Vue 3 用實作帶你看過核心概念 - Day 24：異步加載(Lazy loading)","在大型項目中，我們通常需要將應用拆分為更小的代碼模塊，並僅在需要時才從服務器加載相關組件，以減少初次加載的資源量。Vue 提供了 defineAsyncComponent 方法來實現組件的異步加載，這樣可以有效提高應用的…","2024-10-07T20:28:27+08:00",[11,710,711,725],[11,713],2171,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10364861",{"name":717,"url":718,"day":90},{"path":783,"title":784,"description":785,"date":786,"updated":9,"tags":787,"categories":788,"draft":15,"wordCount":789,"readingTime":318,"canonical":790,"series":791},"\u002Fithome-2024-day-23","Vue 3 用實作帶你看過核心概念 - Day 23：依賴注入( Provide-Inject )","在Dayxx中提到過，父子組件之間的數據傳遞可以通過props來實現。然而，當祖孫層組件需要進行數據傳遞時，中間層級的組件可能並不需要這個props，但卻不得不為了使子組件獲取數據而被迫傳遞。","2024-10-06T15:14:51+08:00",[11,710,711,725],[11,713],3183,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10364252",{"name":717,"url":718,"day":101},{"path":793,"title":794,"description":795,"date":796,"updated":9,"tags":797,"categories":798,"draft":15,"wordCount":799,"readingTime":134,"canonical":800,"series":801},"\u002Fithome-2024-day-22","Vue 3 用實作帶你看過核心概念 - Day 22：v-slot 插槽使用","先前Day 18中提到父組件可以通過props將參數資料傳遞給子組件進行渲染，這樣可以有效地將資料處理與畫面渲染的工作分開。然而，當我們需要更加客製化的呈現內容，由父組件來控制子組件的部分顯示時，可以使用v-slot 指…","2024-10-05T12:32:51+08:00",[11,710,711,725],[11,713],4292,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10363713",{"name":717,"url":718,"day":113},{"path":803,"title":804,"description":805,"date":806,"updated":9,"tags":807,"categories":808,"draft":15,"wordCount":809,"readingTime":329,"canonical":810,"series":811},"\u002Fithome-2024-day-21","Vue 3 用實作帶你看過核心概念 - Day 21： 組件透傳屬性(Attributes)的應用","當父組件向子組件傳遞屬性或監聽器，而這些屬性未在子組件中定義為props或emits時，它們會自動繼承並附加到子組件的根元素上，常見的屬性包括class、style、id等。這些屬性可以透過$attrs屬性訪問並進行操作…","2024-10-04T22:42:54+08:00",[11,710,711,725],[11,713],2146,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10363469",{"name":717,"url":718,"day":125},{"path":813,"title":814,"description":815,"date":816,"updated":9,"tags":817,"categories":818,"draft":15,"wordCount":819,"readingTime":122,"canonical":820,"series":821},"\u002Fithome-2024-day-20","Vue 3 用實作帶你看過核心概念 - Day 20：組件使用 v-model 雙向綁定","在Day 13中，我們介紹了表單元素（例如輸入框）如何處理數據雙向綁定。當需要將表單元素與一個響應式變數綁定時，v-model提供雙向綁定功能。這代表當畫面上表單元素的變動會即時影響Vue數據，同時當Vue數據的值改變時…","2024-10-03T23:40:48+08:00",[11,710,711,725],[11,713],5191,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10362957",{"name":717,"url":718,"day":137},{"path":823,"title":824,"description":825,"date":826,"updated":9,"tags":827,"categories":828,"draft":15,"wordCount":829,"readingTime":318,"canonical":830,"series":831},"\u002Fithome-2024-day-19","Vue 3 用實作帶你看過核心概念 - Day 19：子組件向父組件發射事件 - emits","組件可以通過$emit(functionName, params)自定義事件名稱並傳遞參數。自定義事件名稱建議使用kebab-case來命名自定義事件名稱，以確保命名風格與HTML 屬性一致性。此外，自定義事件還支持事件…","2024-10-02T15:51:16+08:00",[11,710,711,725],[11,713],2877,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10362144",{"name":717,"url":718,"day":148},{"path":833,"title":834,"description":835,"date":836,"updated":9,"tags":837,"categories":838,"draft":15,"wordCount":839,"readingTime":31,"canonical":840,"series":841},"\u002Fithome-2024-day-18","Vue 3 用實作帶你看過核心概念 - Day 18：父組件向子組件傳值 - props","組件之間的傳值必須透過聲明 props 的方式，這樣可以避免與HTML的屬性(attribute)混淆。父組件傳遞參數給子組件的方式有以下兩種方式：","2024-10-01T22:28:48+08:00",[11,710,711,725],[11,713],5782,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10361721",{"name":717,"url":718,"day":159},{"path":843,"title":844,"description":845,"date":846,"updated":9,"tags":847,"categories":848,"draft":15,"wordCount":849,"readingTime":318,"canonical":850,"series":851},"\u002Fithome-2024-day-17","Vue 3 用實作帶你看過核心概念 - Day 17：組件基礎 - 全局註冊與區域註冊","在 Day 5 我們提到可以將應用拆分為根組件和多個組件實例，並將功能模塊化。","2024-09-30T23:30:10+08:00",[11,710,711,725],[11,713],2954,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10361253",{"name":717,"url":718,"day":170},{"path":853,"title":854,"description":855,"date":856,"updated":9,"tags":857,"categories":858,"draft":15,"wordCount":859,"readingTime":122,"canonical":860,"series":861},"\u002Fithome-2024-day-16","Vue 3 用實作帶你看過核心概念 - Day 16：使用 ref 屬性引用模板元素","雖然 Vue 透過模板幫助我們處理了大部分的DOM 操作，但在某些情況下，仍然需要手動操作 DOM。例如，在使用第三方套件（如 Bootstrap 的 Modal）時，或者需要調用像video.play()和video.…","2024-09-29T16:25:45+08:00",[11,710,711,725],[11,713],4608,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10360419",{"name":717,"url":718,"day":181},{"path":863,"title":864,"description":865,"date":866,"updated":9,"tags":867,"categories":868,"draft":15,"wordCount":488,"readingTime":122,"canonical":869,"series":870},"\u002Fithome-2024-day-15","Vue 3 用實作帶你看過核心概念 - Day 15：監聽器(Watchers)","在響應式狀態變化時執行特定操作，需要使用watch 監聽器。這些操作可能包含副作用 (side effects)，如發送請求、調用 API，或更新 DOM 以外的系統狀態，亦或是處理自定義的邏輯。","2024-09-28T18:30:31+08:00",[11,710,711,725],[11,713],"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10359958",{"name":717,"url":718,"day":192},{"path":872,"title":873,"description":874,"date":875,"updated":9,"tags":876,"categories":877,"draft":15,"wordCount":878,"readingTime":134,"canonical":879,"series":880},"\u002Fithome-2024-day-14","Vue 3 用實作帶你看過核心概念 - Day 14：生命週期鉤子(Lifecycle Hooks)","在 Vue 中，當一個組件創建時，它會經歷一系列的流程，這些流程決定了組件從初始化到渲染，再到最終銷毀的過程。","2024-09-27T21:57:32+08:00",[11,710,711,725],[11,713],3780,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10359419",{"name":717,"url":718,"day":203},{"path":882,"title":883,"description":884,"date":885,"updated":9,"tags":886,"categories":887,"draft":15,"wordCount":888,"readingTime":318,"canonical":889,"series":890},"\u002Fithome-2024-day-13","Vue 3 用實作帶你看過核心概念 - Day 13：v-model 雙向綁定控制 - 常用於表單輸入","表單輸入是開發中非常常見的需求，原始方式通常需要手動使用v-bind綁定表單元素的值 (value)，並透過v-on綁定input事件來監聽輸入變化。","2024-09-26T21:49:25+08:00",[11,710,711,725],[11,713],3153,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10358850",{"name":717,"url":718,"day":214},{"path":892,"title":893,"description":894,"date":895,"updated":9,"tags":896,"categories":897,"draft":15,"wordCount":898,"readingTime":134,"canonical":899,"series":900},"\u002Fithome-2024-day-12","Vue 3 用實作帶你看過核心概念 - Day 12：v-on 指令 - 事件處理","在 Vue 中，監聽 HTML DOM 元素的事件變化，可以使用v-on 指令將 JavaScript 事件處理器綁定到對應的DOM 事件上。這個指令的縮寫符號為@，例如：v-on:click，其縮寫方式為@click。","2024-09-25T21:42:59+08:00",[11,710,711,725],[11,713],3808,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10358241",{"name":717,"url":718,"day":225},{"path":902,"title":903,"description":904,"date":905,"updated":9,"tags":906,"categories":907,"draft":15,"wordCount":908,"readingTime":134,"canonical":909,"series":910},"\u002Fithome-2024-day-11","Vue 3 用實作帶你看過核心概念 - Day 11：v-for 指令的基本與進階應用","當數據類型是陣列或物件時，我們不會希望將重複的選項或元素直接硬編碼在模板中。這樣的寫法會導致代碼冗長且不具可維護性。","2024-09-24T23:11:25+08:00",[11,710,711,725],[11,713],4381,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10357705",{"name":717,"url":718,"day":236},{"path":912,"title":913,"description":914,"date":915,"updated":9,"tags":916,"categories":917,"draft":15,"wordCount":918,"readingTime":318,"canonical":919,"series":920},"\u002Fithome-2024-day-10","Vue 3 用實作帶你看過核心概念 - Day 10：條件渲染 - v-if 與 v-show 的區別與應用","開發中遇到需要處理【如果是...就要...】的情境時，Vue 提供了 v-if 指令來幫助我們根據條件進行元素的渲染。這樣可以在特定條件滿足時動態地顯示或隱藏某些元素。","2024-09-23T23:39:24+08:00",[11,710,711,725],[11,713],2827,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10357160",{"name":717,"url":718,"day":247},{"path":922,"title":923,"description":924,"date":925,"updated":9,"tags":926,"categories":927,"draft":15,"wordCount":928,"readingTime":122,"canonical":929,"series":930},"\u002Fithome-2024-day-09","Vue 3 用實作帶你看過核心概念 - Day 9：Class 與 Style 的樣式綁定","v-bind 指令通常用於綁定 HTML 屬性，但在實際開發中，最常見的應用場景之一還是綁定class和 style這兩個 HTML 屬性。","2024-09-22T15:05:53+08:00",[11,710,711,725],[11,713],4630,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10356119",{"name":717,"url":718,"day":76},{"path":932,"title":933,"description":934,"date":935,"updated":9,"tags":936,"categories":937,"draft":15,"wordCount":938,"readingTime":134,"canonical":939,"series":940},"\u002Fithome-2024-day-08","Vue 3 用實作帶你看過核心概念 - Day 8：計算屬性應用與實踐","在 Day 6 在使用 Vue 的Mustache語法時，可以用來放置表達式。不過，這邊建議避免在 Mustache 中撰寫過於複雜的邏輯。這樣的建議主要有兩個原因：","2024-09-21T21:33:29+08:00",[11,710,711,725],[11,713],4060,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10355474",{"name":717,"url":718,"day":110},{"path":942,"title":943,"description":944,"date":945,"updated":9,"tags":946,"categories":947,"draft":15,"wordCount":948,"readingTime":122,"canonical":949,"series":950},"\u002Fithome-2024-day-07","Vue 3 用實作帶你看過核心概念 - Day 7： 響應式基礎 - Options API","前面提到了模板搭配指令及 Mustache 的用法。今天則是要介紹所謂透過getter跟setter取值跟賦值的方式在 JavaScript 是怎麼做到的，這部分會跟 Vue 本身的響應式基礎有很大相關。此外，我也會帶著…","2024-09-20T23:41:42+08:00",[11,710,711],[11,713],4407,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10354781",{"name":717,"url":718,"day":17},{"path":952,"title":953,"description":954,"date":955,"updated":9,"tags":956,"categories":957,"draft":15,"wordCount":958,"readingTime":134,"canonical":959,"series":960},"\u002Fithome-2024-day-06","Vue 3 用實作帶你看過核心概念 - Day 6：數據綁定和模板(Template)","Vue 使用基於 HTML 的模板語法（Template Syntax），將 Vue 實例中的數據綁定到模板中進行顯示。根據綁定的類型不同，數據可以影響元素的內容、屬性或樣式。","2024-09-19T20:39:46+08:00",[11,710,711,725],[11,713],3402,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10354231",{"name":717,"url":718,"day":31},{"path":962,"title":963,"description":964,"date":965,"updated":9,"tags":966,"categories":967,"draft":15,"wordCount":968,"readingTime":318,"canonical":969,"series":970},"\u002Fithome-2024-day-05","Vue 3 用實作帶你看過核心概念 - Day 5：Options API vs Composition API","前面已經詳細說明了 Vue 的環境建置方式，接下來將逐步介紹 Vue 應用說明，並比較Options API與Composition API的撰寫風格。","2024-09-18T22:10:36+08:00",[11,710,711,725,726],[11,713],2902,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10353717",{"name":717,"url":718,"day":122},{"path":972,"title":973,"description":974,"date":975,"updated":9,"tags":976,"categories":977,"draft":15,"wordCount":658,"readingTime":318,"canonical":978,"series":979},"\u002Fithome-2024-day-04","Vue 3 用實作帶你看過核心概念 - Day 4：基於 Vite 建構工具的 Vue 專案結構解析","在 Day 3 的教學中，學習了如何使用 Vue 鷹架工具(Vue project scaffolding tool)來生成一個基於 Vite 環境的專案。今天，我將更進一步地解析這個專案的資料夾結構，幫助大家更好地理解…","2024-09-17T23:27:07+08:00",[11,710,711,725],[11,713],"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10352470",{"name":717,"url":718,"day":134},{"path":981,"title":982,"description":983,"date":984,"updated":9,"tags":985,"categories":986,"draft":15,"wordCount":987,"readingTime":329,"canonical":988,"series":989},"\u002Fithome-2024-day-03","Vue 3 用實作帶你看過核心概念 - Day 3：使用 Vite 建構工具搭建 Vue 專案","⭐ 需要特別注意，需要事先安裝Nodejs。若不確定是否已安裝 Nodejs 也可以開啟終端機，執行以下指令確認版本。","2024-09-16T23:29:45+08:00",[11,710,711,725],[11,713],2100,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10351398",{"name":717,"url":718,"day":318},{"path":991,"title":992,"description":993,"date":994,"updated":9,"tags":995,"categories":996,"draft":15,"wordCount":997,"readingTime":329,"canonical":998,"series":999},"\u002Fithome-2024-day-02","Vue 3 用實作帶你看過核心概念 - Day 2：命令式編程 vs 聲明式編程","還記得我們在使用純 HTML 和 JavaScript 時，如何實現按下按鈕來變更文字顏色嗎？讓我們一起回想一下什麼是命令式宣告。","2024-09-15T22:52:27+08:00",[11,710,711,725,726],[11,713],1881,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10350459",{"name":717,"url":718,"day":329},{"path":1001,"title":1002,"description":1003,"date":1004,"updated":9,"tags":1005,"categories":1006,"draft":15,"wordCount":1007,"readingTime":340,"canonical":1008,"series":1009},"\u002Fithome-2024-day-01","Vue 3 用實作帶你看過核心概念 - Day 1：前言介紹","大家好，我是 Antonio(安東尼奧)，目前是一名 Angular 的新手工程師，同時，也是一名熱愛寫程式的工程師。去年參加六角學院的 Vue 直播班，在那時候就深深著迷著 Vue 的語言，自此這半年多的時間裡，我白天…","2024-09-14T17:32:19+08:00",[11,710,711],[11,713],988,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10350457",{"name":717,"url":718,"day":340},{"path":1011,"title":1012,"description":1013,"date":1014,"updated":9,"tags":1015,"categories":1017,"draft":15,"wordCount":1019,"readingTime":329,"canonical":9,"series":9},"\u002Fhexschool-experience","六角學院 - 軟體工程師體驗營","這麼多線上課程? 你不想知道什麼是最吸引我的線上學習課程嗎?","2024-05-31T21:40:05+08:00",[686,1016],"軟體工程師體驗營",[689,1018],"前端成長記",1109,{"path":1021,"title":1022,"description":1023,"date":1024,"updated":9,"tags":1025,"categories":1033,"draft":15,"wordCount":1035,"readingTime":318,"canonical":9,"series":9},"\u002FJS-10","JavaScript：var、let、const之間的差異解析","var、let及const是JavaScript中宣告變數的關鍵字，它們在作用域、對於for迴圈的影響、提升 (Hoisting) 的差異及重複宣告的處理上有所不同。","2023-12-10T23:53:30+08:00",[1026,1027,1028,1029,1030,1031,1032],"ES6","var","let","const","Hoisting","Scope","TDZ",[1034,702],"JavaScript",2873,{"path":1037,"title":1038,"description":1039,"date":1040,"updated":9,"tags":1041,"categories":1048,"draft":15,"wordCount":1049,"readingTime":122,"canonical":9,"series":9},"\u002FJS-9","JavaScript：陣列的常見操作方法","掌握 JavaScript 陣列操作！從forEach、map、filter、findIndex、reduce、sort，深入了解這些方法，使你的程式碼更具靈活性和效率！","2023-12-01T15:01:06+08:00",[1042,1043,1044,1045,1046,1047],"forEach","map","filter","reduce","sort","findIndex",[1034,702],4846,{"path":1051,"title":1052,"description":1053,"date":1054,"updated":9,"tags":1055,"categories":1060,"draft":15,"wordCount":1061,"readingTime":318,"canonical":9,"series":9},"\u002FJS-8","JavaScript：ES module 匯入與匯出","使用\u003C script >標籤讓瀏覽器中的網頁能夠使用 ES 模組功能時，你了解不同的匯入及匯出方法了嗎？","2023-11-30T15:28:23+08:00",[1026,1056,1057,1058,1059],"ES Module","CommonJS","Node.js","模組化開發",[1034,702],2913,{"path":1063,"title":1064,"description":1065,"date":1066,"updated":9,"tags":1067,"categories":1070,"draft":15,"wordCount":1072,"readingTime":134,"canonical":9,"series":9},"\u002FReact-2","React：JSX 是什麼？為何在 React 中使用 JSX？","透過 JSX 語法糖，你能夠在一次 JavaScript 撰寫中包含 HTML 與 CSS 的內容","2023-11-29T16:03:40+08:00",[1068,1034,1069],"JSX","Babel",[1071,702],"React",3595,{"path":1074,"title":1075,"description":1076,"date":1077,"updated":9,"tags":1078,"categories":1081,"draft":15,"wordCount":1082,"readingTime":329,"canonical":9,"series":9},"\u002FReact-1","React：Pure Function 的概念及其在 React 中的應用","探索 Pure Function 概念及其與 React 的相互關係。","2023-11-29T15:50:17+08:00",[1034,1079,1080],"Pure Function","Functional Programming",[1071],1368,{"path":1084,"title":1085,"description":1086,"date":1087,"updated":9,"tags":1088,"categories":1093,"draft":15,"wordCount":1094,"readingTime":122,"canonical":9,"series":9},"\u002FJS-6","JavaScript：深入理解Promise的原理及實際應用","透過Promise，我們能夠有效解決JavaScript中的異步事件，使得程式碼更為清晰和可讀。","2023-11-29T14:49:18+08:00",[1089,1090,1091,1092],"Promise","AsyncAwait","Asynchronous Programming","異步處理",[1034,702],5479,{"path":1096,"title":1097,"description":1098,"date":1099,"updated":9,"tags":1100,"categories":1105,"draft":15,"wordCount":1106,"readingTime":122,"canonical":9,"series":9},"\u002FJS-5","JavaScript：物件傳參考特性(Pass by Reference)","淺拷貝(shallow copy)與深拷貝(deep copy)在JavaScript中的應用與理解","2023-11-29T09:55:50+08:00",[1101,1102,1103,1104],"Pass by Reference","Shallow Copy","Deep Copy","傳值與傳參考",[1034,702],4544,{"path":1108,"title":1109,"description":1110,"date":1111,"updated":9,"tags":1112,"categories":1118,"draft":15,"wordCount":1120,"readingTime":329,"canonical":9,"series":9},"\u002FJS-4","JavaScript：注重關注點的分離","透過關注點分離的設計方式，可顯著提高系統整體的可維護性。","2023-11-29T09:44:29+08:00",[1113,1114,1115,1116,1117],"SoC","MVC","Design Patterns","Clean Code","Refactoring",[1034,1119],"Software Engineering",1977,{"path":1122,"title":1123,"description":1124,"date":1125,"updated":9,"tags":1126,"categories":1132,"draft":15,"wordCount":1133,"readingTime":329,"canonical":9,"series":9},"\u002FJS-3","JavaScript：掌握箭頭函式，優雅簡約的程式寫作","深入解析ES6箭頭函數：揭開與傳統函式的差異。","2023-11-29T01:02:36+08:00",[1127,1128,1129,1130,1131],"Arrow Function","this","Lexical Scoping","Syntactic Sugar","Callback Function",[1034,702],2142,{"path":1135,"title":1136,"description":1137,"date":1138,"updated":9,"tags":1139,"categories":1143,"draft":15,"wordCount":1144,"readingTime":329,"canonical":9,"series":9},"\u002FJS-2","JavaScript：深入了解This的指向與應用","探索JavaScript This 的指向：深入剖析判斷的技巧。","2023-11-28T22:22:45+08:00",[1128,1140,1131,1141,1142],"Execution Context","Binding","Fundamentals",[1034,702],1963,{"path":1146,"title":1147,"description":1148,"date":1149,"updated":9,"tags":1150,"categories":1154,"draft":15,"wordCount":1155,"readingTime":340,"canonical":9,"series":9},"\u002FJS-1","JavaScript：淺談ES6常見縮寫","JavaScript ES6縮寫技巧：輕鬆簡化程式碼，提升效率！。","2023-11-28T13:38:14+08:00",[1026,1151,1152,1153],"Shorthand","Spread Operator","Object",[1034,702],818,{"path":1157,"title":1158,"description":1159,"date":1160,"updated":9,"tags":1161,"categories":1165,"draft":15,"wordCount":1166,"readingTime":340,"canonical":9,"series":9},"\u002FEmmet-1","前端開發神器：使用Emmet提升網頁開發效率","本文將介紹如何利用Emmet來大幅提高前端開發效率，讓你的編碼速度如專家般飛快。","2023-10-31T14:40:54+08:00",[1162,1163,1164],"Emmet","VSCode","HTML",[702,673],865,{"path":1168,"title":1169,"description":1170,"date":1171,"updated":9,"tags":1172,"categories":1177,"draft":15,"wordCount":1179,"readingTime":340,"canonical":9,"series":9},"\u002Fhexo-github-4","建立自己的部落格：了解Hexo指令，更換next部落格主題","今天的你跟我都是Hexo指令指揮官，讓我們將部落格造型大改造一下吧!!!","2023-10-20T14:33:38+08:00",[1173,1174,1175,1176],"Hexo","NexT","git clone","Hexo指令",[1178],"Blog",979,{"path":1181,"title":1182,"description":1183,"date":1184,"updated":9,"tags":1185,"categories":1189,"draft":15,"wordCount":1190,"readingTime":329,"canonical":9,"series":9},"\u002Fhexo-github-3","建立自己的部落格：修改Hexo設定檔中Site，查看其變化","今天的你跟我都是部落格工程師，我們一起來將網頁做一些小小的改變，有一天匯集在一起就會讓你的部落格變的與眾不同。","2023-10-18T20:50:31+08:00",[1173,1163,1186,1187,1188],"Sublime Text","YAML","SEO",[1178],1798,{"path":1192,"title":1193,"description":1194,"date":1195,"updated":9,"tags":1196,"categories":1199,"draft":15,"wordCount":1200,"readingTime":340,"canonical":9,"series":9},"\u002Fhexo-github-2","建立自己的部落格：創建Hexo Blog初始的模板","踏出那關鍵的第一步，從白紙到完整的部落格，一同見證你的部落格誕生的奇妙時刻。","2023-10-17T15:59:37+08:00",[1173,1197,1198],"Hexo-cli","hexo init",[1178],941,{"path":1202,"title":1203,"description":1204,"date":1205,"updated":9,"tags":1206,"categories":1208,"draft":15,"wordCount":1209,"readingTime":340,"canonical":9,"series":9},"\u002Fhexo-github-1","建立自己的部落格：認識Hexo及所其需工具(Nodejs & Git)","打造屬於自己風格的部落格其實不難，讓我們一步步手把手教你如何實現這個目標。","2023-10-16T08:30:27+08:00",[1173,1058,1207],"Git",[1178],790,{"id":1211,"title":59,"body":1212,"canonical":65,"categories":4169,"date":61,"description":60,"draft":15,"extension":4170,"meta":4171,"navigation":1282,"ogImage":9,"path":58,"rawbody":4172,"readingTime":17,"robots":9,"schemaOrg":9,"seo":4173,"series":4174,"sitemap":4175,"slug":4180,"stem":4181,"tags":4182,"updated":9,"wordCount":64,"__hash__":4183},"posts\u002F2026-10-10-ithome-2026-day-26.md",{"type":1213,"value":1214,"toc":4153},"minimark",[1215,1218,1221,1385,1388,1396,1401,1404,1407,1420,1423,1427,1432,1699,1710,1717,1723,1730,1734,1737,1744,1748,1753,2071,2083,2096,2111,2115,2127,2134,2139,2207,2216,2223,2393,2416,2425,2463,2476,2485,2491,2495,2505,2510,3244,3247,3303,3315,3331,3335,3338,3343,3847,3859,3862,3912,3915,3921,3933,3937,3943,3978,3990,4033,4040,4053,4063,4066,4069,4075,4089,4095,4098,4101,4149],[1216,1217,60],"p",{},[1216,1219,1220],{},"不過，當購物車需要根據不同會員套用折扣時，可能會直接在 Store 裡面增加判斷：",[1222,1223,1228],"pre",{"className":1224,"code":1225,"language":1226,"meta":1227,"style":1227},"language-ts shiki shiki-themes github-light github-dark-dimmed","const finalTotal = computed(() => {\n  const total = store.total();\n\n  if (memberType() === 'vip') {\n    return total * 0.9;\n  }\n\n  if (memberType() === 'business') {\n    return total * 0.85;\n  }\n\n  return total;\n});\n","ts","",[1229,1230,1231,1259,1278,1284,1308,1325,1330,1334,1351,1364,1368,1372,1380],"code",{"__ignoreMap":1227},[1232,1233,1235,1238,1242,1245,1249,1253,1256],"span",{"class":1234,"line":340},"line",[1232,1236,1029],{"class":1237},"s7YZ4",[1232,1239,1241],{"class":1240},"sviXB"," finalTotal",[1232,1243,1244],{"class":1237}," =",[1232,1246,1248],{"class":1247},"sPO5f"," computed",[1232,1250,1252],{"class":1251},"sYgZi","(() ",[1232,1254,1255],{"class":1237},"=>",[1232,1257,1258],{"class":1251}," {\n",[1232,1260,1261,1264,1267,1269,1272,1275],{"class":1234,"line":329},[1232,1262,1263],{"class":1237},"  const",[1232,1265,1266],{"class":1240}," total",[1232,1268,1244],{"class":1237},[1232,1270,1271],{"class":1251}," store.",[1232,1273,1274],{"class":1247},"total",[1232,1276,1277],{"class":1251},"();\n",[1232,1279,1280],{"class":1234,"line":318},[1232,1281,1283],{"emptyLinePlaceholder":1282},true,"\n",[1232,1285,1286,1289,1292,1295,1298,1301,1305],{"class":1234,"line":134},[1232,1287,1288],{"class":1237},"  if",[1232,1290,1291],{"class":1251}," (",[1232,1293,1294],{"class":1247},"memberType",[1232,1296,1297],{"class":1251},"() ",[1232,1299,1300],{"class":1237},"===",[1232,1302,1304],{"class":1303},"s-HuK"," 'vip'",[1232,1306,1307],{"class":1251},") {\n",[1232,1309,1310,1313,1316,1319,1322],{"class":1234,"line":122},[1232,1311,1312],{"class":1237},"    return",[1232,1314,1315],{"class":1251}," total ",[1232,1317,1318],{"class":1237},"*",[1232,1320,1321],{"class":1240}," 0.9",[1232,1323,1324],{"class":1251},";\n",[1232,1326,1327],{"class":1234,"line":31},[1232,1328,1329],{"class":1251},"  }\n",[1232,1331,1332],{"class":1234,"line":17},[1232,1333,1283],{"emptyLinePlaceholder":1282},[1232,1335,1336,1338,1340,1342,1344,1346,1349],{"class":1234,"line":110},[1232,1337,1288],{"class":1237},[1232,1339,1291],{"class":1251},[1232,1341,1294],{"class":1247},[1232,1343,1297],{"class":1251},[1232,1345,1300],{"class":1237},[1232,1347,1348],{"class":1303}," 'business'",[1232,1350,1307],{"class":1251},[1232,1352,1353,1355,1357,1359,1362],{"class":1234,"line":76},[1232,1354,1312],{"class":1237},[1232,1356,1315],{"class":1251},[1232,1358,1318],{"class":1237},[1232,1360,1361],{"class":1240}," 0.85",[1232,1363,1324],{"class":1251},[1232,1365,1366],{"class":1234,"line":247},[1232,1367,1329],{"class":1251},[1232,1369,1370],{"class":1234,"line":236},[1232,1371,1283],{"emptyLinePlaceholder":1282},[1232,1373,1374,1377],{"class":1234,"line":225},[1232,1375,1376],{"class":1237},"  return",[1232,1378,1379],{"class":1251}," total;\n",[1232,1381,1382],{"class":1234,"line":214},[1232,1383,1384],{"class":1251},"});\n",[1216,1386,1387],{},"目前只有三種會員，這樣寫沒有什麼問題。但如果未來增加更多折扣規則，Store 就需要不斷修改。",[1216,1389,1390,1391,1395],{},"今天我們會使用 ",[1392,1393,1394],"strong",{},"Strategy Pattern（策略模式）"," 拆分折扣計算，再搭配 Angular Dependency Injection（DI）與 Signal，讓會員類型改變時，可以動態切換對應的折扣規則。",[1397,1398,1400],"h2",{"id":1399},"strategy-pattern-是什麼","Strategy Pattern 是什麼？",[1216,1402,1403],{},"Strategy Pattern 是一種行為型設計模式，主要是將不同的演算法或業務規則封裝成獨立物件，讓使用端透過共同介面操作。",[1216,1405,1406],{},"以購物車為例，假設有三種會員：",[1408,1409,1410,1414,1417],"ul",{},[1411,1412,1413],"li",{},"一般會員：原價。",[1411,1415,1416],{},"VIP 會員：9 折。",[1411,1418,1419],{},"企業會員：85 折。",[1216,1421,1422],{},"三種會員的計算方式不同，但都需要根據總金額計算折扣，因此可以先定義共同介面。",[1424,1425,1426],"h3",{"id":1426},"定義折扣策略",[1216,1428,1429],{},[1229,1430,1431],{},"discount.strategy.ts",[1222,1433,1435],{"className":1224,"code":1434,"language":1226,"meta":1227,"style":1227},"import { Injectable } from '@angular\u002Fcore';\n\nexport interface DiscountStrategy {\n  calculate(total: number): number;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class NormalDiscount implements DiscountStrategy {\n  calculate(total: number) {\n    return total;\n  }\n}\n\n@Injectable({ providedIn: 'root' })\nexport class VipDiscount implements DiscountStrategy {\n  calculate(total: number) {\n    return total * 0.9;\n  }\n}\n\n@Injectable({ providedIn: 'root' })\nexport class BusinessDiscount implements DiscountStrategy {\n  calculate(total: number) {\n    return total * 0.85;\n  }\n}\n",[1229,1436,1437,1453,1457,1471,1497,1502,1506,1523,1541,1555,1561,1565,1569,1573,1585,1600,1614,1626,1630,1634,1638,1650,1665,1679,1691,1695],{"__ignoreMap":1227},[1232,1438,1439,1442,1445,1448,1451],{"class":1234,"line":340},[1232,1440,1441],{"class":1237},"import",[1232,1443,1444],{"class":1251}," { Injectable } ",[1232,1446,1447],{"class":1237},"from",[1232,1449,1450],{"class":1303}," '@angular\u002Fcore'",[1232,1452,1324],{"class":1251},[1232,1454,1455],{"class":1234,"line":329},[1232,1456,1283],{"emptyLinePlaceholder":1282},[1232,1458,1459,1462,1465,1469],{"class":1234,"line":318},[1232,1460,1461],{"class":1237},"export",[1232,1463,1464],{"class":1237}," interface",[1232,1466,1468],{"class":1467},"sOLd2"," DiscountStrategy",[1232,1470,1258],{"class":1251},[1232,1472,1473,1476,1479,1482,1485,1488,1491,1493,1495],{"class":1234,"line":134},[1232,1474,1475],{"class":1247},"  calculate",[1232,1477,1478],{"class":1251},"(",[1232,1480,1274],{"class":1481},"stnAF",[1232,1483,1484],{"class":1237},":",[1232,1486,1487],{"class":1240}," number",[1232,1489,1490],{"class":1251},")",[1232,1492,1484],{"class":1237},[1232,1494,1487],{"class":1240},[1232,1496,1324],{"class":1251},[1232,1498,1499],{"class":1234,"line":122},[1232,1500,1501],{"class":1251},"}\n",[1232,1503,1504],{"class":1234,"line":31},[1232,1505,1283],{"emptyLinePlaceholder":1282},[1232,1507,1508,1511,1514,1517,1520],{"class":1234,"line":17},[1232,1509,1510],{"class":1251},"@",[1232,1512,1513],{"class":1247},"Injectable",[1232,1515,1516],{"class":1251},"({ providedIn: ",[1232,1518,1519],{"class":1303},"'root'",[1232,1521,1522],{"class":1251}," })\n",[1232,1524,1525,1527,1530,1533,1536,1539],{"class":1234,"line":110},[1232,1526,1461],{"class":1237},[1232,1528,1529],{"class":1237}," class",[1232,1531,1532],{"class":1467}," NormalDiscount",[1232,1534,1535],{"class":1237}," implements",[1232,1537,1468],{"class":1538},"sMOND",[1232,1540,1258],{"class":1251},[1232,1542,1543,1545,1547,1549,1551,1553],{"class":1234,"line":76},[1232,1544,1475],{"class":1247},[1232,1546,1478],{"class":1251},[1232,1548,1274],{"class":1481},[1232,1550,1484],{"class":1237},[1232,1552,1487],{"class":1240},[1232,1554,1307],{"class":1251},[1232,1556,1557,1559],{"class":1234,"line":247},[1232,1558,1312],{"class":1237},[1232,1560,1379],{"class":1251},[1232,1562,1563],{"class":1234,"line":236},[1232,1564,1329],{"class":1251},[1232,1566,1567],{"class":1234,"line":225},[1232,1568,1501],{"class":1251},[1232,1570,1571],{"class":1234,"line":214},[1232,1572,1283],{"emptyLinePlaceholder":1282},[1232,1574,1575,1577,1579,1581,1583],{"class":1234,"line":203},[1232,1576,1510],{"class":1251},[1232,1578,1513],{"class":1247},[1232,1580,1516],{"class":1251},[1232,1582,1519],{"class":1303},[1232,1584,1522],{"class":1251},[1232,1586,1587,1589,1591,1594,1596,1598],{"class":1234,"line":192},[1232,1588,1461],{"class":1237},[1232,1590,1529],{"class":1237},[1232,1592,1593],{"class":1467}," VipDiscount",[1232,1595,1535],{"class":1237},[1232,1597,1468],{"class":1538},[1232,1599,1258],{"class":1251},[1232,1601,1602,1604,1606,1608,1610,1612],{"class":1234,"line":181},[1232,1603,1475],{"class":1247},[1232,1605,1478],{"class":1251},[1232,1607,1274],{"class":1481},[1232,1609,1484],{"class":1237},[1232,1611,1487],{"class":1240},[1232,1613,1307],{"class":1251},[1232,1615,1616,1618,1620,1622,1624],{"class":1234,"line":170},[1232,1617,1312],{"class":1237},[1232,1619,1315],{"class":1251},[1232,1621,1318],{"class":1237},[1232,1623,1321],{"class":1240},[1232,1625,1324],{"class":1251},[1232,1627,1628],{"class":1234,"line":159},[1232,1629,1329],{"class":1251},[1232,1631,1632],{"class":1234,"line":148},[1232,1633,1501],{"class":1251},[1232,1635,1636],{"class":1234,"line":137},[1232,1637,1283],{"emptyLinePlaceholder":1282},[1232,1639,1640,1642,1644,1646,1648],{"class":1234,"line":125},[1232,1641,1510],{"class":1251},[1232,1643,1513],{"class":1247},[1232,1645,1516],{"class":1251},[1232,1647,1519],{"class":1303},[1232,1649,1522],{"class":1251},[1232,1651,1652,1654,1656,1659,1661,1663],{"class":1234,"line":113},[1232,1653,1461],{"class":1237},[1232,1655,1529],{"class":1237},[1232,1657,1658],{"class":1467}," BusinessDiscount",[1232,1660,1535],{"class":1237},[1232,1662,1468],{"class":1538},[1232,1664,1258],{"class":1251},[1232,1666,1667,1669,1671,1673,1675,1677],{"class":1234,"line":101},[1232,1668,1475],{"class":1247},[1232,1670,1478],{"class":1251},[1232,1672,1274],{"class":1481},[1232,1674,1484],{"class":1237},[1232,1676,1487],{"class":1240},[1232,1678,1307],{"class":1251},[1232,1680,1681,1683,1685,1687,1689],{"class":1234,"line":90},[1232,1682,1312],{"class":1237},[1232,1684,1315],{"class":1251},[1232,1686,1318],{"class":1237},[1232,1688,1361],{"class":1240},[1232,1690,1324],{"class":1251},[1232,1692,1693],{"class":1234,"line":79},[1232,1694,1329],{"class":1251},[1232,1696,1697],{"class":1234,"line":67},[1232,1698,1501],{"class":1251},[1216,1700,1701,1702,1705,1706,1709],{},"每個 Class 都實作 ",[1229,1703,1704],{},"DiscountStrategy","，提供相同的 ",[1229,1707,1708],{},"calculate()"," 方法，但使用不同的折扣規則。未來新增其他折扣時，只需要建立新的 Strategy，不必將所有判斷集中在 Store 裡。",[1216,1711,1712,1713,1716],{},"這裡透過 ",[1229,1714,1715],{},"@Injectable({ providedIn: 'root' })"," 將三個 Strategy 註冊到 Angular DI。它們通常會在第一次被注入時才建立實體，之後由 Root Injector 共用，而不是 Application 啟動時就全部建立。",[1216,1718,1719,1720,1722],{},"下圖整理三個 Strategy 與共同介面的關係，無論使用哪種折扣規則，都能透過相同的 ",[1229,1721,1708],{}," 方法計算。",[1216,1724,1725],{},[1726,1727],"img",{"alt":1728,"src":1729},"Strategy Pattern 折扣策略：三個 Strategy 實作同一個 DiscountStrategy 介面","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791640682\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2026%EF%BC%9ASignalStore%20%E8%88%87%20Strategy%20Pattern%EF%BC%8C%E9%80%8F%E9%81%8E%20DI%20%E5%8B%95%E6%85%8B%E5%88%87%E6%8F%9B%E6%A5%AD%E5%8B%99%E9%82%8F%E8%BC%AF\u002Fstrategy-pattern-discounts.png",[1397,1731,1733],{"id":1732},"使用-service-動態切換-strategy","使用 Service 動態切換 Strategy",[1216,1735,1736],{},"將折扣規則拆開後，還需要根據目前的會員類型，選擇要使用哪一種 Strategy。",[1216,1738,1739,1740,1743],{},"這次建立 ",[1229,1741,1742],{},"DiscountService","，透過 Signal 記錄會員類型，並利用 Angular DI 取得三個折扣策略。",[1424,1745,1747],{"id":1746},"建立-discountservice","建立 DiscountService",[1216,1749,1750],{},[1229,1751,1752],{},"discount.service.ts",[1222,1754,1756],{"className":1224,"code":1755,"language":1226,"meta":1227,"style":1227},"import { Injectable, inject, signal } from '@angular\u002Fcore';\n\nimport {\n  NormalDiscount,\n  VipDiscount,\n  BusinessDiscount\n} from '.\u002Fdiscount.strategy';\n\nimport type { DiscountStrategy } from '.\u002Fdiscount.strategy';\n\nexport type MemberType = 'normal' | 'vip' | 'business';\n\n@Injectable({ providedIn: 'root' })\nexport class DiscountService implements DiscountStrategy {\n  readonly memberType = signal\u003CMemberType>('normal');\n\n  private readonly strategies = {\n    normal: inject(NormalDiscount),\n    vip: inject(VipDiscount),\n    business: inject(BusinessDiscount)\n  };\n\n  calculate(total: number) {\n    return this.strategies[\n      this.memberType()\n    ].calculate(total);\n  }\n\n  change(type: MemberType) {\n    this.memberType.set(type);\n  }\n}\n",[1229,1757,1758,1771,1775,1781,1786,1791,1796,1808,1812,1828,1832,1857,1861,1873,1888,1916,1920,1935,1946,1956,1966,1971,1975,1989,1999,2012,2023,2027,2031,2047,2061,2066],{"__ignoreMap":1227},[1232,1759,1760,1762,1765,1767,1769],{"class":1234,"line":340},[1232,1761,1441],{"class":1237},[1232,1763,1764],{"class":1251}," { Injectable, inject, signal } ",[1232,1766,1447],{"class":1237},[1232,1768,1450],{"class":1303},[1232,1770,1324],{"class":1251},[1232,1772,1773],{"class":1234,"line":329},[1232,1774,1283],{"emptyLinePlaceholder":1282},[1232,1776,1777,1779],{"class":1234,"line":318},[1232,1778,1441],{"class":1237},[1232,1780,1258],{"class":1251},[1232,1782,1783],{"class":1234,"line":134},[1232,1784,1785],{"class":1251},"  NormalDiscount,\n",[1232,1787,1788],{"class":1234,"line":122},[1232,1789,1790],{"class":1251},"  VipDiscount,\n",[1232,1792,1793],{"class":1234,"line":31},[1232,1794,1795],{"class":1251},"  BusinessDiscount\n",[1232,1797,1798,1801,1803,1806],{"class":1234,"line":17},[1232,1799,1800],{"class":1251},"} ",[1232,1802,1447],{"class":1237},[1232,1804,1805],{"class":1303}," '.\u002Fdiscount.strategy'",[1232,1807,1324],{"class":1251},[1232,1809,1810],{"class":1234,"line":110},[1232,1811,1283],{"emptyLinePlaceholder":1282},[1232,1813,1814,1816,1819,1822,1824,1826],{"class":1234,"line":76},[1232,1815,1441],{"class":1237},[1232,1817,1818],{"class":1237}," type",[1232,1820,1821],{"class":1251}," { DiscountStrategy } ",[1232,1823,1447],{"class":1237},[1232,1825,1805],{"class":1303},[1232,1827,1324],{"class":1251},[1232,1829,1830],{"class":1234,"line":247},[1232,1831,1283],{"emptyLinePlaceholder":1282},[1232,1833,1834,1836,1838,1841,1843,1846,1849,1851,1853,1855],{"class":1234,"line":236},[1232,1835,1461],{"class":1237},[1232,1837,1818],{"class":1237},[1232,1839,1840],{"class":1467}," MemberType",[1232,1842,1244],{"class":1237},[1232,1844,1845],{"class":1303}," 'normal'",[1232,1847,1848],{"class":1237}," |",[1232,1850,1304],{"class":1303},[1232,1852,1848],{"class":1237},[1232,1854,1348],{"class":1303},[1232,1856,1324],{"class":1251},[1232,1858,1859],{"class":1234,"line":225},[1232,1860,1283],{"emptyLinePlaceholder":1282},[1232,1862,1863,1865,1867,1869,1871],{"class":1234,"line":214},[1232,1864,1510],{"class":1251},[1232,1866,1513],{"class":1247},[1232,1868,1516],{"class":1251},[1232,1870,1519],{"class":1303},[1232,1872,1522],{"class":1251},[1232,1874,1875,1877,1879,1882,1884,1886],{"class":1234,"line":203},[1232,1876,1461],{"class":1237},[1232,1878,1529],{"class":1237},[1232,1880,1881],{"class":1467}," DiscountService",[1232,1883,1535],{"class":1237},[1232,1885,1468],{"class":1538},[1232,1887,1258],{"class":1251},[1232,1889,1890,1893,1896,1898,1901,1904,1907,1910,1913],{"class":1234,"line":192},[1232,1891,1892],{"class":1237},"  readonly",[1232,1894,1895],{"class":1481}," memberType",[1232,1897,1244],{"class":1237},[1232,1899,1900],{"class":1247}," signal",[1232,1902,1903],{"class":1251},"\u003C",[1232,1905,1906],{"class":1467},"MemberType",[1232,1908,1909],{"class":1251},">(",[1232,1911,1912],{"class":1303},"'normal'",[1232,1914,1915],{"class":1251},");\n",[1232,1917,1918],{"class":1234,"line":181},[1232,1919,1283],{"emptyLinePlaceholder":1282},[1232,1921,1922,1925,1928,1931,1933],{"class":1234,"line":170},[1232,1923,1924],{"class":1237},"  private",[1232,1926,1927],{"class":1237}," readonly",[1232,1929,1930],{"class":1481}," strategies",[1232,1932,1244],{"class":1237},[1232,1934,1258],{"class":1251},[1232,1936,1937,1940,1943],{"class":1234,"line":159},[1232,1938,1939],{"class":1251},"    normal: ",[1232,1941,1942],{"class":1247},"inject",[1232,1944,1945],{"class":1251},"(NormalDiscount),\n",[1232,1947,1948,1951,1953],{"class":1234,"line":148},[1232,1949,1950],{"class":1251},"    vip: ",[1232,1952,1942],{"class":1247},[1232,1954,1955],{"class":1251},"(VipDiscount),\n",[1232,1957,1958,1961,1963],{"class":1234,"line":137},[1232,1959,1960],{"class":1251},"    business: ",[1232,1962,1942],{"class":1247},[1232,1964,1965],{"class":1251},"(BusinessDiscount)\n",[1232,1967,1968],{"class":1234,"line":125},[1232,1969,1970],{"class":1251},"  };\n",[1232,1972,1973],{"class":1234,"line":113},[1232,1974,1283],{"emptyLinePlaceholder":1282},[1232,1976,1977,1979,1981,1983,1985,1987],{"class":1234,"line":101},[1232,1978,1475],{"class":1247},[1232,1980,1478],{"class":1251},[1232,1982,1274],{"class":1481},[1232,1984,1484],{"class":1237},[1232,1986,1487],{"class":1240},[1232,1988,1307],{"class":1251},[1232,1990,1991,1993,1996],{"class":1234,"line":90},[1232,1992,1312],{"class":1237},[1232,1994,1995],{"class":1240}," this",[1232,1997,1998],{"class":1251},".strategies[\n",[1232,2000,2001,2004,2007,2009],{"class":1234,"line":79},[1232,2002,2003],{"class":1240},"      this",[1232,2005,2006],{"class":1251},".",[1232,2008,1294],{"class":1247},[1232,2010,2011],{"class":1251},"()\n",[1232,2013,2014,2017,2020],{"class":1234,"line":67},[1232,2015,2016],{"class":1251},"    ].",[1232,2018,2019],{"class":1247},"calculate",[1232,2021,2022],{"class":1251},"(total);\n",[1232,2024,2025],{"class":1234,"line":56},[1232,2026,1329],{"class":1251},[1232,2028,2029],{"class":1234,"line":45},[1232,2030,1283],{"emptyLinePlaceholder":1282},[1232,2032,2033,2036,2038,2041,2043,2045],{"class":1234,"line":34},[1232,2034,2035],{"class":1247},"  change",[1232,2037,1478],{"class":1251},[1232,2039,2040],{"class":1481},"type",[1232,2042,1484],{"class":1237},[1232,2044,1840],{"class":1467},[1232,2046,1307],{"class":1251},[1232,2048,2049,2052,2055,2058],{"class":1234,"line":22},[1232,2050,2051],{"class":1240},"    this",[1232,2053,2054],{"class":1251},".memberType.",[1232,2056,2057],{"class":1247},"set",[1232,2059,2060],{"class":1251},"(type);\n",[1232,2062,2064],{"class":1234,"line":2063},31,[1232,2065,1329],{"class":1251},[1232,2067,2069],{"class":1234,"line":2068},32,[1232,2070,1501],{"class":1251},[1216,2072,2073,2075,2076,2078,2079,2082],{},[1229,2074,1742],{}," 本身也實作了 ",[1229,2077,1704],{},"，但不直接計算折扣，而是根據 ",[1229,2080,2081],{},"memberType()"," 選擇對應的 Strategy。",[1216,2084,1712,2085,2088,2089,2091,2092,2095],{},[1229,2086,2087],{},"inject()"," 取得三個 Strategy，因此當 ",[1229,2090,1742],{}," 第一次建立時，就會一併取得這些實體。之後呼叫 ",[1229,2093,2094],{},"change()"," 切換會員類型時，只需要改用對應的 Strategy，不需要重新建立。",[1216,2097,2098,2099,2102,2103,2106,2107,2110],{},"例如目前是 VIP 會員，就使用 ",[1229,2100,2101],{},"VipDiscount","；當呼叫 ",[1229,2104,2105],{},"change('business')"," 後，便改由 ",[1229,2108,2109],{},"BusinessDiscount"," 計算。",[1397,2112,2114],{"id":2113},"透過-angular-di-提供-strategy","透過 Angular DI 提供 Strategy",[1216,2116,2117,2118,2120,2121,2123,2124,2126],{},"有了 ",[1229,2119,1742],{}," 後，我們希望 SignalStore 只需要使用 ",[1229,2122,1708],{},"，不必直接依賴 ",[1229,2125,1742],{}," 這個 Class。",[1216,2128,2129,2130,2133],{},"由於 TypeScript Interface 在編譯後不會保留，無法直接作為 Angular DI 的 Token，因此需要建立 ",[1229,2131,2132],{},"InjectionToken","。",[1216,2135,2136],{},[1229,2137,2138],{},"discount.token.ts",[1222,2140,2142],{"className":1224,"code":2141,"language":1226,"meta":1227,"style":1227},"import { InjectionToken } from '@angular\u002Fcore';\nimport type { DiscountStrategy } from '.\u002Fdiscount.strategy';\n\nexport const DISCOUNT_STRATEGY =\n  new InjectionToken\u003CDiscountStrategy>('DISCOUNT_STRATEGY');\n",[1229,2143,2144,2157,2171,2175,2188],{"__ignoreMap":1227},[1232,2145,2146,2148,2151,2153,2155],{"class":1234,"line":340},[1232,2147,1441],{"class":1237},[1232,2149,2150],{"class":1251}," { InjectionToken } ",[1232,2152,1447],{"class":1237},[1232,2154,1450],{"class":1303},[1232,2156,1324],{"class":1251},[1232,2158,2159,2161,2163,2165,2167,2169],{"class":1234,"line":329},[1232,2160,1441],{"class":1237},[1232,2162,1818],{"class":1237},[1232,2164,1821],{"class":1251},[1232,2166,1447],{"class":1237},[1232,2168,1805],{"class":1303},[1232,2170,1324],{"class":1251},[1232,2172,2173],{"class":1234,"line":318},[1232,2174,1283],{"emptyLinePlaceholder":1282},[1232,2176,2177,2179,2182,2185],{"class":1234,"line":134},[1232,2178,1461],{"class":1237},[1232,2180,2181],{"class":1237}," const",[1232,2183,2184],{"class":1240}," DISCOUNT_STRATEGY",[1232,2186,2187],{"class":1237}," =\n",[1232,2189,2190,2193,2196,2198,2200,2202,2205],{"class":1234,"line":122},[1232,2191,2192],{"class":1237},"  new",[1232,2194,2195],{"class":1247}," InjectionToken",[1232,2197,1903],{"class":1251},[1232,2199,1704],{"class":1467},[1232,2201,1909],{"class":1251},[1232,2203,2204],{"class":1303},"'DISCOUNT_STRATEGY'",[1232,2206,1915],{"class":1251},[1216,2208,2209,2212,2213,2215],{},[1229,2210,2211],{},"DISCOUNT_STRATEGY"," 是注入時使用的 Token，泛型則指定取得的物件需要符合 ",[1229,2214,1704],{}," 介面。",[1216,2217,2218,2219,2222],{},"接著在 ",[1229,2220,2221],{},"app.config.ts"," 註冊：",[1222,2224,2226],{"className":1224,"code":2225,"language":1226,"meta":1227,"style":1227},"import { ApplicationConfig } from '@angular\u002Fcore';\nimport { provideRouter } from '@angular\u002Frouter';\nimport { provideHttpClient } from '@angular\u002Fcommon\u002Fhttp';\n\nimport { routes } from '.\u002Fapp.routes';\nimport { DISCOUNT_STRATEGY } from '.\u002Fcart\u002Fdiscount.token';\nimport { DiscountService } from '.\u002Fcart\u002Fdiscount.service';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideRouter(routes),\n    provideHttpClient(),\n    {\n      provide: DISCOUNT_STRATEGY,\n      useExisting: DiscountService\n    }\n  ]\n};\n",[1229,2227,2228,2241,2255,2269,2273,2287,2301,2315,2319,2337,2342,2350,2358,2363,2373,2378,2383,2388],{"__ignoreMap":1227},[1232,2229,2230,2232,2235,2237,2239],{"class":1234,"line":340},[1232,2231,1441],{"class":1237},[1232,2233,2234],{"class":1251}," { ApplicationConfig } ",[1232,2236,1447],{"class":1237},[1232,2238,1450],{"class":1303},[1232,2240,1324],{"class":1251},[1232,2242,2243,2245,2248,2250,2253],{"class":1234,"line":329},[1232,2244,1441],{"class":1237},[1232,2246,2247],{"class":1251}," { provideRouter } ",[1232,2249,1447],{"class":1237},[1232,2251,2252],{"class":1303}," '@angular\u002Frouter'",[1232,2254,1324],{"class":1251},[1232,2256,2257,2259,2262,2264,2267],{"class":1234,"line":318},[1232,2258,1441],{"class":1237},[1232,2260,2261],{"class":1251}," { provideHttpClient } ",[1232,2263,1447],{"class":1237},[1232,2265,2266],{"class":1303}," '@angular\u002Fcommon\u002Fhttp'",[1232,2268,1324],{"class":1251},[1232,2270,2271],{"class":1234,"line":134},[1232,2272,1283],{"emptyLinePlaceholder":1282},[1232,2274,2275,2277,2280,2282,2285],{"class":1234,"line":122},[1232,2276,1441],{"class":1237},[1232,2278,2279],{"class":1251}," { routes } ",[1232,2281,1447],{"class":1237},[1232,2283,2284],{"class":1303}," '.\u002Fapp.routes'",[1232,2286,1324],{"class":1251},[1232,2288,2289,2291,2294,2296,2299],{"class":1234,"line":31},[1232,2290,1441],{"class":1237},[1232,2292,2293],{"class":1251}," { DISCOUNT_STRATEGY } ",[1232,2295,1447],{"class":1237},[1232,2297,2298],{"class":1303}," '.\u002Fcart\u002Fdiscount.token'",[1232,2300,1324],{"class":1251},[1232,2302,2303,2305,2308,2310,2313],{"class":1234,"line":17},[1232,2304,1441],{"class":1237},[1232,2306,2307],{"class":1251}," { DiscountService } ",[1232,2309,1447],{"class":1237},[1232,2311,2312],{"class":1303}," '.\u002Fcart\u002Fdiscount.service'",[1232,2314,1324],{"class":1251},[1232,2316,2317],{"class":1234,"line":110},[1232,2318,1283],{"emptyLinePlaceholder":1282},[1232,2320,2321,2323,2325,2328,2330,2333,2335],{"class":1234,"line":76},[1232,2322,1461],{"class":1237},[1232,2324,2181],{"class":1237},[1232,2326,2327],{"class":1240}," appConfig",[1232,2329,1484],{"class":1237},[1232,2331,2332],{"class":1467}," ApplicationConfig",[1232,2334,1244],{"class":1237},[1232,2336,1258],{"class":1251},[1232,2338,2339],{"class":1234,"line":247},[1232,2340,2341],{"class":1251},"  providers: [\n",[1232,2343,2344,2347],{"class":1234,"line":236},[1232,2345,2346],{"class":1247},"    provideRouter",[1232,2348,2349],{"class":1251},"(routes),\n",[1232,2351,2352,2355],{"class":1234,"line":225},[1232,2353,2354],{"class":1247},"    provideHttpClient",[1232,2356,2357],{"class":1251},"(),\n",[1232,2359,2360],{"class":1234,"line":214},[1232,2361,2362],{"class":1251},"    {\n",[1232,2364,2365,2368,2370],{"class":1234,"line":203},[1232,2366,2367],{"class":1251},"      provide: ",[1232,2369,2211],{"class":1240},[1232,2371,2372],{"class":1251},",\n",[1232,2374,2375],{"class":1234,"line":192},[1232,2376,2377],{"class":1251},"      useExisting: DiscountService\n",[1232,2379,2380],{"class":1234,"line":181},[1232,2381,2382],{"class":1251},"    }\n",[1232,2384,2385],{"class":1234,"line":170},[1232,2386,2387],{"class":1251},"  ]\n",[1232,2389,2390],{"class":1234,"line":159},[1232,2391,2392],{"class":1251},"};\n",[1216,2394,2395,2396,2399,2400,2402,2403,2405,2406,2408,2409,2412,2413,2133],{},"這裡使用 ",[1229,2397,2398],{},"useExisting","，讓 ",[1229,2401,2211],{}," 成為 ",[1229,2404,1742],{}," 的另一個注入名稱。由於 ",[1229,2407,1742],{}," 已經透過 ",[1229,2410,2411],{},"providedIn: 'root'"," 註冊，因此不需要再加入 ",[1229,2414,2415],{},"providers",[1216,2417,2418,2420,2421,2424],{},[1229,2419,2398],{}," 與 ",[1229,2422,2423],{},"useClass"," 的差別在於是否共用實體：",[2426,2427,2428,2441],"table",{},[2429,2430,2431],"thead",{},[2432,2433,2434,2438],"tr",{},[2435,2436,2437],"th",{},"Provider",[2435,2439,2440],{},"說明",[2442,2443,2444,2454],"tbody",{},[2432,2445,2446,2451],{},[2447,2448,2449],"td",{},[1229,2450,2423],{},[2447,2452,2453],{},"由 DI 為該 Token 建立指定 Class 的實體",[2432,2455,2456,2460],{},[2447,2457,2458],{},[1229,2459,2398],{},[2447,2461,2462],{},"使用另一個 Token 對應的同一個實體",[1216,2464,2465,2466,2469,2470,2472,2473,2475],{},"如果改成 ",[1229,2467,2468],{},"useClass: DiscountService","，Angular 就會為 ",[1229,2471,2211],{}," 建立另一個 ",[1229,2474,1742],{}," 實體，與原本透過 Root 註冊的實體分開管理。",[1216,2477,2478,2479,2481,2482,2484],{},"這次使用 ",[1229,2480,2398],{},"，是因為 Component 需要切換會員類型，而 SignalStore 需要根據相同的會員狀態計算折扣，因此兩者必須取得同一個 ",[1229,2483,1742],{}," 實體。",[1216,2486,2487],{},[1726,2488],{"alt":2489,"src":2490},"Angular DI useExisting：CartComponent 與 NgRxCartStore 共用同一個 DiscountService 實體","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791640685\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2026%EF%BC%9ASignalStore%20%E8%88%87%20Strategy%20Pattern%EF%BC%8C%E9%80%8F%E9%81%8E%20DI%20%E5%8B%95%E6%85%8B%E5%88%87%E6%8F%9B%E6%A5%AD%E5%8B%99%E9%82%8F%E8%BC%AF\u002Fdi-use-existing.png",[1397,2492,2494],{"id":2493},"在-signalstore-使用-strategy","在 SignalStore 使用 Strategy",[1216,2496,2497,2498,2501,2502,2133],{},"延續昨天的 ",[1229,2499,2500],{},"NgRxCartStore","，保留原本的購物車功能，另外增加折扣後的 ",[1229,2503,2504],{},"finalTotal",[1216,2506,2507],{},[1229,2508,2509],{},"ngrx-cart.store.ts",[1222,2511,2513],{"className":1224,"code":2512,"language":1226,"meta":1227,"style":1227},"import { computed, inject } from '@angular\u002Fcore';\nimport {\n  signalStore,\n  withState,\n  withComputed,\n  withMethods,\n  patchState\n} from '@ngrx\u002Fsignals';\n\nimport type { CartItem, Product } from '.\u002Fcart.types';\nimport { DISCOUNT_STRATEGY } from '.\u002Fdiscount.token';\n\nexport const NgRxCartStore = signalStore(\n  { providedIn: 'root' },\n\n  withState({\n    items: [] as CartItem[]\n  }),\n\n  withComputed(({ items }) => {\n    const discount = inject(DISCOUNT_STRATEGY);\n\n    const count = computed(() =>\n      items().reduce(\n        (sum, item) => sum + item.quantity,\n        0\n      )\n    );\n\n    const total = computed(() =>\n      items().reduce(\n        (sum, item) => sum + item.price * item.quantity,\n        0\n      )\n    );\n\n    const finalTotal = computed(() =>\n      discount.calculate(total())\n    );\n\n    return { count, total, finalTotal };\n  }),\n\n  withMethods(store => ({\n    add(product: Product) {\n      patchState(store, state => {\n        const existing = state.items.find(\n          item => item.id === product.id\n        );\n\n        const items = existing\n          ? state.items.map(item =>\n              item.id === product.id\n                ? { ...item, quantity: item.quantity + 1 }\n                : item\n            )\n          : [...state.items, { ...product, quantity: 1 }];\n\n        return { items };\n      });\n    },\n\n    remove(id: number) {\n      patchState(store, state => ({\n        items: state.items.filter(item => item.id !== id)\n      }));\n    },\n\n    clear() {\n      patchState(store, { items: [] });\n    }\n  }))\n);\n",[1229,2514,2515,2528,2534,2539,2544,2549,2554,2559,2570,2574,2590,2603,2607,2624,2634,2638,2646,2660,2665,2669,2687,2706,2710,2726,2738,2766,2771,2776,2781,2785,2799,2809,2834,2839,2844,2849,2854,2869,2884,2889,2894,2902,2907,2912,2929,2947,2963,2982,2998,3004,3009,3022,3039,3049,3072,3081,3087,3112,3117,3126,3132,3138,3143,3160,3173,3195,3201,3206,3211,3220,3228,3233,3239],{"__ignoreMap":1227},[1232,2516,2517,2519,2522,2524,2526],{"class":1234,"line":340},[1232,2518,1441],{"class":1237},[1232,2520,2521],{"class":1251}," { computed, inject } ",[1232,2523,1447],{"class":1237},[1232,2525,1450],{"class":1303},[1232,2527,1324],{"class":1251},[1232,2529,2530,2532],{"class":1234,"line":329},[1232,2531,1441],{"class":1237},[1232,2533,1258],{"class":1251},[1232,2535,2536],{"class":1234,"line":318},[1232,2537,2538],{"class":1251},"  signalStore,\n",[1232,2540,2541],{"class":1234,"line":134},[1232,2542,2543],{"class":1251},"  withState,\n",[1232,2545,2546],{"class":1234,"line":122},[1232,2547,2548],{"class":1251},"  withComputed,\n",[1232,2550,2551],{"class":1234,"line":31},[1232,2552,2553],{"class":1251},"  withMethods,\n",[1232,2555,2556],{"class":1234,"line":17},[1232,2557,2558],{"class":1251},"  patchState\n",[1232,2560,2561,2563,2565,2568],{"class":1234,"line":110},[1232,2562,1800],{"class":1251},[1232,2564,1447],{"class":1237},[1232,2566,2567],{"class":1303}," '@ngrx\u002Fsignals'",[1232,2569,1324],{"class":1251},[1232,2571,2572],{"class":1234,"line":76},[1232,2573,1283],{"emptyLinePlaceholder":1282},[1232,2575,2576,2578,2580,2583,2585,2588],{"class":1234,"line":247},[1232,2577,1441],{"class":1237},[1232,2579,1818],{"class":1237},[1232,2581,2582],{"class":1251}," { CartItem, Product } ",[1232,2584,1447],{"class":1237},[1232,2586,2587],{"class":1303}," '.\u002Fcart.types'",[1232,2589,1324],{"class":1251},[1232,2591,2592,2594,2596,2598,2601],{"class":1234,"line":236},[1232,2593,1441],{"class":1237},[1232,2595,2293],{"class":1251},[1232,2597,1447],{"class":1237},[1232,2599,2600],{"class":1303}," '.\u002Fdiscount.token'",[1232,2602,1324],{"class":1251},[1232,2604,2605],{"class":1234,"line":225},[1232,2606,1283],{"emptyLinePlaceholder":1282},[1232,2608,2609,2611,2613,2616,2618,2621],{"class":1234,"line":214},[1232,2610,1461],{"class":1237},[1232,2612,2181],{"class":1237},[1232,2614,2615],{"class":1240}," NgRxCartStore",[1232,2617,1244],{"class":1237},[1232,2619,2620],{"class":1247}," signalStore",[1232,2622,2623],{"class":1251},"(\n",[1232,2625,2626,2629,2631],{"class":1234,"line":203},[1232,2627,2628],{"class":1251},"  { providedIn: ",[1232,2630,1519],{"class":1303},[1232,2632,2633],{"class":1251}," },\n",[1232,2635,2636],{"class":1234,"line":192},[1232,2637,1283],{"emptyLinePlaceholder":1282},[1232,2639,2640,2643],{"class":1234,"line":181},[1232,2641,2642],{"class":1247},"  withState",[1232,2644,2645],{"class":1251},"({\n",[1232,2647,2648,2651,2654,2657],{"class":1234,"line":170},[1232,2649,2650],{"class":1251},"    items: [] ",[1232,2652,2653],{"class":1237},"as",[1232,2655,2656],{"class":1467}," CartItem",[1232,2658,2659],{"class":1251},"[]\n",[1232,2661,2662],{"class":1234,"line":159},[1232,2663,2664],{"class":1251},"  }),\n",[1232,2666,2667],{"class":1234,"line":148},[1232,2668,1283],{"emptyLinePlaceholder":1282},[1232,2670,2671,2674,2677,2680,2683,2685],{"class":1234,"line":137},[1232,2672,2673],{"class":1247},"  withComputed",[1232,2675,2676],{"class":1251},"(({ ",[1232,2678,2679],{"class":1481},"items",[1232,2681,2682],{"class":1251}," }) ",[1232,2684,1255],{"class":1237},[1232,2686,1258],{"class":1251},[1232,2688,2689,2692,2695,2697,2700,2702,2704],{"class":1234,"line":125},[1232,2690,2691],{"class":1237},"    const",[1232,2693,2694],{"class":1240}," discount",[1232,2696,1244],{"class":1237},[1232,2698,2699],{"class":1247}," inject",[1232,2701,1478],{"class":1251},[1232,2703,2211],{"class":1240},[1232,2705,1915],{"class":1251},[1232,2707,2708],{"class":1234,"line":113},[1232,2709,1283],{"emptyLinePlaceholder":1282},[1232,2711,2712,2714,2717,2719,2721,2723],{"class":1234,"line":101},[1232,2713,2691],{"class":1237},[1232,2715,2716],{"class":1240}," count",[1232,2718,1244],{"class":1237},[1232,2720,1248],{"class":1247},[1232,2722,1252],{"class":1251},[1232,2724,2725],{"class":1237},"=>\n",[1232,2727,2728,2731,2734,2736],{"class":1234,"line":90},[1232,2729,2730],{"class":1247},"      items",[1232,2732,2733],{"class":1251},"().",[1232,2735,1045],{"class":1247},[1232,2737,2623],{"class":1251},[1232,2739,2740,2743,2746,2749,2752,2755,2757,2760,2763],{"class":1234,"line":79},[1232,2741,2742],{"class":1251},"        (",[1232,2744,2745],{"class":1481},"sum",[1232,2747,2748],{"class":1251},", ",[1232,2750,2751],{"class":1481},"item",[1232,2753,2754],{"class":1251},") ",[1232,2756,1255],{"class":1237},[1232,2758,2759],{"class":1251}," sum ",[1232,2761,2762],{"class":1237},"+",[1232,2764,2765],{"class":1251}," item.quantity,\n",[1232,2767,2768],{"class":1234,"line":67},[1232,2769,2770],{"class":1240},"        0\n",[1232,2772,2773],{"class":1234,"line":56},[1232,2774,2775],{"class":1251},"      )\n",[1232,2777,2778],{"class":1234,"line":45},[1232,2779,2780],{"class":1251},"    );\n",[1232,2782,2783],{"class":1234,"line":34},[1232,2784,1283],{"emptyLinePlaceholder":1282},[1232,2786,2787,2789,2791,2793,2795,2797],{"class":1234,"line":22},[1232,2788,2691],{"class":1237},[1232,2790,1266],{"class":1240},[1232,2792,1244],{"class":1237},[1232,2794,1248],{"class":1247},[1232,2796,1252],{"class":1251},[1232,2798,2725],{"class":1237},[1232,2800,2801,2803,2805,2807],{"class":1234,"line":2063},[1232,2802,2730],{"class":1247},[1232,2804,2733],{"class":1251},[1232,2806,1045],{"class":1247},[1232,2808,2623],{"class":1251},[1232,2810,2811,2813,2815,2817,2819,2821,2823,2825,2827,2830,2832],{"class":1234,"line":2068},[1232,2812,2742],{"class":1251},[1232,2814,2745],{"class":1481},[1232,2816,2748],{"class":1251},[1232,2818,2751],{"class":1481},[1232,2820,2754],{"class":1251},[1232,2822,1255],{"class":1237},[1232,2824,2759],{"class":1251},[1232,2826,2762],{"class":1237},[1232,2828,2829],{"class":1251}," item.price ",[1232,2831,1318],{"class":1237},[1232,2833,2765],{"class":1251},[1232,2835,2837],{"class":1234,"line":2836},33,[1232,2838,2770],{"class":1240},[1232,2840,2842],{"class":1234,"line":2841},34,[1232,2843,2775],{"class":1251},[1232,2845,2847],{"class":1234,"line":2846},35,[1232,2848,2780],{"class":1251},[1232,2850,2852],{"class":1234,"line":2851},36,[1232,2853,1283],{"emptyLinePlaceholder":1282},[1232,2855,2857,2859,2861,2863,2865,2867],{"class":1234,"line":2856},37,[1232,2858,2691],{"class":1237},[1232,2860,1241],{"class":1240},[1232,2862,1244],{"class":1237},[1232,2864,1248],{"class":1247},[1232,2866,1252],{"class":1251},[1232,2868,2725],{"class":1237},[1232,2870,2872,2875,2877,2879,2881],{"class":1234,"line":2871},38,[1232,2873,2874],{"class":1251},"      discount.",[1232,2876,2019],{"class":1247},[1232,2878,1478],{"class":1251},[1232,2880,1274],{"class":1247},[1232,2882,2883],{"class":1251},"())\n",[1232,2885,2887],{"class":1234,"line":2886},39,[1232,2888,2780],{"class":1251},[1232,2890,2892],{"class":1234,"line":2891},40,[1232,2893,1283],{"emptyLinePlaceholder":1282},[1232,2895,2897,2899],{"class":1234,"line":2896},41,[1232,2898,1312],{"class":1237},[1232,2900,2901],{"class":1251}," { count, total, finalTotal };\n",[1232,2903,2905],{"class":1234,"line":2904},42,[1232,2906,2664],{"class":1251},[1232,2908,2910],{"class":1234,"line":2909},43,[1232,2911,1283],{"emptyLinePlaceholder":1282},[1232,2913,2915,2918,2920,2923,2926],{"class":1234,"line":2914},44,[1232,2916,2917],{"class":1247},"  withMethods",[1232,2919,1478],{"class":1251},[1232,2921,2922],{"class":1481},"store",[1232,2924,2925],{"class":1237}," =>",[1232,2927,2928],{"class":1251}," ({\n",[1232,2930,2932,2935,2937,2940,2942,2945],{"class":1234,"line":2931},45,[1232,2933,2934],{"class":1247},"    add",[1232,2936,1478],{"class":1251},[1232,2938,2939],{"class":1481},"product",[1232,2941,1484],{"class":1237},[1232,2943,2944],{"class":1467}," Product",[1232,2946,1307],{"class":1251},[1232,2948,2950,2953,2956,2959,2961],{"class":1234,"line":2949},46,[1232,2951,2952],{"class":1247},"      patchState",[1232,2954,2955],{"class":1251},"(store, ",[1232,2957,2958],{"class":1481},"state",[1232,2960,2925],{"class":1237},[1232,2962,1258],{"class":1251},[1232,2964,2966,2969,2972,2974,2977,2980],{"class":1234,"line":2965},47,[1232,2967,2968],{"class":1237},"        const",[1232,2970,2971],{"class":1240}," existing",[1232,2973,1244],{"class":1237},[1232,2975,2976],{"class":1251}," state.items.",[1232,2978,2979],{"class":1247},"find",[1232,2981,2623],{"class":1251},[1232,2983,2985,2988,2990,2993,2995],{"class":1234,"line":2984},48,[1232,2986,2987],{"class":1481},"          item",[1232,2989,2925],{"class":1237},[1232,2991,2992],{"class":1251}," item.id ",[1232,2994,1300],{"class":1237},[1232,2996,2997],{"class":1251}," product.id\n",[1232,2999,3001],{"class":1234,"line":3000},49,[1232,3002,3003],{"class":1251},"        );\n",[1232,3005,3007],{"class":1234,"line":3006},50,[1232,3008,1283],{"emptyLinePlaceholder":1282},[1232,3010,3012,3014,3017,3019],{"class":1234,"line":3011},51,[1232,3013,2968],{"class":1237},[1232,3015,3016],{"class":1240}," items",[1232,3018,1244],{"class":1237},[1232,3020,3021],{"class":1251}," existing\n",[1232,3023,3025,3028,3030,3032,3034,3036],{"class":1234,"line":3024},52,[1232,3026,3027],{"class":1237},"          ?",[1232,3029,2976],{"class":1251},[1232,3031,1043],{"class":1247},[1232,3033,1478],{"class":1251},[1232,3035,2751],{"class":1481},[1232,3037,3038],{"class":1237}," =>\n",[1232,3040,3042,3045,3047],{"class":1234,"line":3041},53,[1232,3043,3044],{"class":1251},"              item.id ",[1232,3046,1300],{"class":1237},[1232,3048,2997],{"class":1251},[1232,3050,3052,3055,3058,3061,3064,3066,3069],{"class":1234,"line":3051},54,[1232,3053,3054],{"class":1237},"                ?",[1232,3056,3057],{"class":1251}," { ",[1232,3059,3060],{"class":1237},"...",[1232,3062,3063],{"class":1251},"item, quantity: item.quantity ",[1232,3065,2762],{"class":1237},[1232,3067,3068],{"class":1240}," 1",[1232,3070,3071],{"class":1251}," }\n",[1232,3073,3075,3078],{"class":1234,"line":3074},55,[1232,3076,3077],{"class":1237},"                :",[1232,3079,3080],{"class":1251}," item\n",[1232,3082,3084],{"class":1234,"line":3083},56,[1232,3085,3086],{"class":1251},"            )\n",[1232,3088,3090,3093,3096,3098,3101,3103,3106,3109],{"class":1234,"line":3089},57,[1232,3091,3092],{"class":1237},"          :",[1232,3094,3095],{"class":1251}," [",[1232,3097,3060],{"class":1237},[1232,3099,3100],{"class":1251},"state.items, { ",[1232,3102,3060],{"class":1237},[1232,3104,3105],{"class":1251},"product, quantity: ",[1232,3107,3108],{"class":1240},"1",[1232,3110,3111],{"class":1251}," }];\n",[1232,3113,3115],{"class":1234,"line":3114},58,[1232,3116,1283],{"emptyLinePlaceholder":1282},[1232,3118,3120,3123],{"class":1234,"line":3119},59,[1232,3121,3122],{"class":1237},"        return",[1232,3124,3125],{"class":1251}," { items };\n",[1232,3127,3129],{"class":1234,"line":3128},60,[1232,3130,3131],{"class":1251},"      });\n",[1232,3133,3135],{"class":1234,"line":3134},61,[1232,3136,3137],{"class":1251},"    },\n",[1232,3139,3141],{"class":1234,"line":3140},62,[1232,3142,1283],{"emptyLinePlaceholder":1282},[1232,3144,3146,3149,3151,3154,3156,3158],{"class":1234,"line":3145},63,[1232,3147,3148],{"class":1247},"    remove",[1232,3150,1478],{"class":1251},[1232,3152,3153],{"class":1481},"id",[1232,3155,1484],{"class":1237},[1232,3157,1487],{"class":1240},[1232,3159,1307],{"class":1251},[1232,3161,3163,3165,3167,3169,3171],{"class":1234,"line":3162},64,[1232,3164,2952],{"class":1247},[1232,3166,2955],{"class":1251},[1232,3168,2958],{"class":1481},[1232,3170,2925],{"class":1237},[1232,3172,2928],{"class":1251},[1232,3174,3176,3179,3181,3183,3185,3187,3189,3192],{"class":1234,"line":3175},65,[1232,3177,3178],{"class":1251},"        items: state.items.",[1232,3180,1044],{"class":1247},[1232,3182,1478],{"class":1251},[1232,3184,2751],{"class":1481},[1232,3186,2925],{"class":1237},[1232,3188,2992],{"class":1251},[1232,3190,3191],{"class":1237},"!==",[1232,3193,3194],{"class":1251}," id)\n",[1232,3196,3198],{"class":1234,"line":3197},66,[1232,3199,3200],{"class":1251},"      }));\n",[1232,3202,3204],{"class":1234,"line":3203},67,[1232,3205,3137],{"class":1251},[1232,3207,3209],{"class":1234,"line":3208},68,[1232,3210,1283],{"emptyLinePlaceholder":1282},[1232,3212,3214,3217],{"class":1234,"line":3213},69,[1232,3215,3216],{"class":1247},"    clear",[1232,3218,3219],{"class":1251},"() {\n",[1232,3221,3223,3225],{"class":1234,"line":3222},70,[1232,3224,2952],{"class":1247},[1232,3226,3227],{"class":1251},"(store, { items: [] });\n",[1232,3229,3231],{"class":1234,"line":3230},71,[1232,3232,2382],{"class":1251},[1232,3234,3236],{"class":1234,"line":3235},72,[1232,3237,3238],{"class":1251},"  }))\n",[1232,3240,3242],{"class":1234,"line":3241},73,[1232,3243,1915],{"class":1251},[1216,3245,3246],{},"這次主要新增：",[1222,3248,3250],{"className":1224,"code":3249,"language":1226,"meta":1227,"style":1227},"const discount = inject(DISCOUNT_STRATEGY);\n\nconst finalTotal = computed(() =>\n  discount.calculate(total())\n);\n",[1229,3251,3252,3268,3272,3286,3299],{"__ignoreMap":1227},[1232,3253,3254,3256,3258,3260,3262,3264,3266],{"class":1234,"line":340},[1232,3255,1029],{"class":1237},[1232,3257,2694],{"class":1240},[1232,3259,1244],{"class":1237},[1232,3261,2699],{"class":1247},[1232,3263,1478],{"class":1251},[1232,3265,2211],{"class":1240},[1232,3267,1915],{"class":1251},[1232,3269,3270],{"class":1234,"line":329},[1232,3271,1283],{"emptyLinePlaceholder":1282},[1232,3273,3274,3276,3278,3280,3282,3284],{"class":1234,"line":318},[1232,3275,1029],{"class":1237},[1232,3277,1241],{"class":1240},[1232,3279,1244],{"class":1237},[1232,3281,1248],{"class":1247},[1232,3283,1252],{"class":1251},[1232,3285,2725],{"class":1237},[1232,3287,3288,3291,3293,3295,3297],{"class":1234,"line":134},[1232,3289,3290],{"class":1251},"  discount.",[1232,3292,2019],{"class":1247},[1232,3294,1478],{"class":1251},[1232,3296,1274],{"class":1247},[1232,3298,2883],{"class":1251},[1232,3300,3301],{"class":1234,"line":122},[1232,3302,1915],{"class":1251},[1216,3304,3305,3308,3309,3311,3312,3314],{},[1229,3306,3307],{},"inject(DISCOUNT_STRATEGY)"," 會透過 Angular DI 取得前面註冊的 ",[1229,3310,1742],{}," 實體，再由 ",[1229,3313,1708],{}," 計算折扣後的金額。",[1216,3316,3317,3318,3321,3322,3324,3325,3327,3328,3330],{},"這裡也可以直接使用 ",[1229,3319,3320],{},"inject(DiscountService)","，但透過 ",[1229,3323,2211],{},"，Store 只需要知道 ",[1229,3326,1708],{},"，不必依賴 ",[1229,3329,1742],{}," 的其他功能。未來如果更換成其他符合介面的折扣服務，也不需要修改 Store。",[1397,3332,3334],{"id":3333},"component-切換會員類型","Component 切換會員類型",[1216,3336,3337],{},"最後在 Component 加入會員選單，讓使用者可以切換折扣。",[1216,3339,3340],{},[1229,3341,3342],{},"cart.ts",[1222,3344,3346],{"className":1224,"code":3345,"language":1226,"meta":1227,"style":1227},"import { Component, inject } from '@angular\u002Fcore';\n\nimport { NgRxCartStore } from '.\u002Fngrx-cart.store';\nimport { DiscountService } from '.\u002Fdiscount.service';\nimport type { MemberType } from '.\u002Fdiscount.service';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003Cmain style=\"padding: 24px; max-width: 600px;\">\n      \u003Ch1>Strategy Pattern 購物車\u003C\u002Fh1>\n\n      \u003Ch3>商品列表\u003C\u002Fh3>\n\n      \u003Cbutton (click)=\"store.add({\n        id: 1,\n        name: '鍵盤',\n        price: 1200\n      })\">\n        加入鍵盤 $1200\n      \u003C\u002Fbutton>\n\n      \u003Cbutton (click)=\"store.add({\n        id: 2,\n        name: '滑鼠',\n        price: 800\n      })\">\n        加入滑鼠 $800\n      \u003C\u002Fbutton>\n\n      \u003Chr \u002F>\n\n      \u003Ch3>會員類型\u003C\u002Fh3>\n\n      \u003Cselect\n        [value]=\"discount.memberType()\"\n        (change)=\"changeMember($event)\"\n      >\n        \u003Coption value=\"normal\">一般會員\u003C\u002Foption>\n        \u003Coption value=\"vip\">VIP 會員\u003C\u002Foption>\n        \u003Coption value=\"business\">企業會員\u003C\u002Foption>\n      \u003C\u002Fselect>\n\n      \u003Chr \u002F>\n\n      \u003Ch3>購物車\u003C\u002Fh3>\n\n      @for (item of store.items(); track item.id) {\n        \u003Cp>\n          {{ item.name }} × {{ item.quantity }}\n          = {{ item.price * item.quantity }} 元\n\n          \u003Cbutton (click)=\"store.remove(item.id)\">\n            移除\n          \u003C\u002Fbutton>\n        \u003C\u002Fp>\n      } @empty {\n        \u003Cp>購物車目前沒有商品\u003C\u002Fp>\n      }\n\n      \u003Chr \u002F>\n\n      \u003Cp>商品數量：{{ store.count() }}\u003C\u002Fp>\n      \u003Cp>原價：{{ store.total() }} 元\u003C\u002Fp>\n      \u003Ch2>折扣後：{{ store.finalTotal() }} 元\u003C\u002Fh2>\n\n      \u003Cbutton (click)=\"store.clear()\">\n        清空購物車\n      \u003C\u002Fbutton>\n    \u003C\u002Fmain>\n  `\n})\nexport class CartComponent {\n  readonly store = inject(NgRxCartStore);\n  readonly discount = inject(DiscountService);\n\n  changeMember(event: Event) {\n    const select = event.target as HTMLSelectElement;\n    this.discount.change(select.value as MemberType);\n  }\n}\n",[1229,3347,3348,3361,3365,3379,3392,3407,3411,3420,3430,3438,3443,3448,3452,3457,3461,3466,3471,3476,3481,3486,3491,3496,3500,3504,3509,3514,3519,3523,3528,3532,3536,3541,3545,3550,3554,3559,3564,3569,3574,3579,3584,3589,3594,3598,3602,3606,3611,3615,3620,3625,3630,3635,3639,3644,3649,3654,3659,3664,3669,3674,3678,3682,3686,3691,3696,3701,3705,3710,3715,3719,3724,3729,3734,3745,3760,3774,3779,3797,3817,3837,3842],{"__ignoreMap":1227},[1232,3349,3350,3352,3355,3357,3359],{"class":1234,"line":340},[1232,3351,1441],{"class":1237},[1232,3353,3354],{"class":1251}," { Component, inject } ",[1232,3356,1447],{"class":1237},[1232,3358,1450],{"class":1303},[1232,3360,1324],{"class":1251},[1232,3362,3363],{"class":1234,"line":329},[1232,3364,1283],{"emptyLinePlaceholder":1282},[1232,3366,3367,3369,3372,3374,3377],{"class":1234,"line":318},[1232,3368,1441],{"class":1237},[1232,3370,3371],{"class":1251}," { NgRxCartStore } ",[1232,3373,1447],{"class":1237},[1232,3375,3376],{"class":1303}," '.\u002Fngrx-cart.store'",[1232,3378,1324],{"class":1251},[1232,3380,3381,3383,3385,3387,3390],{"class":1234,"line":134},[1232,3382,1441],{"class":1237},[1232,3384,2307],{"class":1251},[1232,3386,1447],{"class":1237},[1232,3388,3389],{"class":1303}," '.\u002Fdiscount.service'",[1232,3391,1324],{"class":1251},[1232,3393,3394,3396,3398,3401,3403,3405],{"class":1234,"line":122},[1232,3395,1441],{"class":1237},[1232,3397,1818],{"class":1237},[1232,3399,3400],{"class":1251}," { MemberType } ",[1232,3402,1447],{"class":1237},[1232,3404,3389],{"class":1303},[1232,3406,1324],{"class":1251},[1232,3408,3409],{"class":1234,"line":31},[1232,3410,1283],{"emptyLinePlaceholder":1282},[1232,3412,3413,3415,3418],{"class":1234,"line":17},[1232,3414,1510],{"class":1251},[1232,3416,3417],{"class":1247},"Component",[1232,3419,2645],{"class":1251},[1232,3421,3422,3425,3428],{"class":1234,"line":110},[1232,3423,3424],{"class":1251},"  selector: ",[1232,3426,3427],{"class":1303},"'app-root'",[1232,3429,2372],{"class":1251},[1232,3431,3432,3435],{"class":1234,"line":76},[1232,3433,3434],{"class":1251},"  template: ",[1232,3436,3437],{"class":1303},"`\n",[1232,3439,3440],{"class":1234,"line":247},[1232,3441,3442],{"class":1303},"    \u003Cmain style=\"padding: 24px; max-width: 600px;\">\n",[1232,3444,3445],{"class":1234,"line":236},[1232,3446,3447],{"class":1303},"      \u003Ch1>Strategy Pattern 購物車\u003C\u002Fh1>\n",[1232,3449,3450],{"class":1234,"line":225},[1232,3451,1283],{"emptyLinePlaceholder":1282},[1232,3453,3454],{"class":1234,"line":214},[1232,3455,3456],{"class":1303},"      \u003Ch3>商品列表\u003C\u002Fh3>\n",[1232,3458,3459],{"class":1234,"line":203},[1232,3460,1283],{"emptyLinePlaceholder":1282},[1232,3462,3463],{"class":1234,"line":192},[1232,3464,3465],{"class":1303},"      \u003Cbutton (click)=\"store.add({\n",[1232,3467,3468],{"class":1234,"line":181},[1232,3469,3470],{"class":1303},"        id: 1,\n",[1232,3472,3473],{"class":1234,"line":170},[1232,3474,3475],{"class":1303},"        name: '鍵盤',\n",[1232,3477,3478],{"class":1234,"line":159},[1232,3479,3480],{"class":1303},"        price: 1200\n",[1232,3482,3483],{"class":1234,"line":148},[1232,3484,3485],{"class":1303},"      })\">\n",[1232,3487,3488],{"class":1234,"line":137},[1232,3489,3490],{"class":1303},"        加入鍵盤 $1200\n",[1232,3492,3493],{"class":1234,"line":125},[1232,3494,3495],{"class":1303},"      \u003C\u002Fbutton>\n",[1232,3497,3498],{"class":1234,"line":113},[1232,3499,1283],{"emptyLinePlaceholder":1282},[1232,3501,3502],{"class":1234,"line":101},[1232,3503,3465],{"class":1303},[1232,3505,3506],{"class":1234,"line":90},[1232,3507,3508],{"class":1303},"        id: 2,\n",[1232,3510,3511],{"class":1234,"line":79},[1232,3512,3513],{"class":1303},"        name: '滑鼠',\n",[1232,3515,3516],{"class":1234,"line":67},[1232,3517,3518],{"class":1303},"        price: 800\n",[1232,3520,3521],{"class":1234,"line":56},[1232,3522,3485],{"class":1303},[1232,3524,3525],{"class":1234,"line":45},[1232,3526,3527],{"class":1303},"        加入滑鼠 $800\n",[1232,3529,3530],{"class":1234,"line":34},[1232,3531,3495],{"class":1303},[1232,3533,3534],{"class":1234,"line":22},[1232,3535,1283],{"emptyLinePlaceholder":1282},[1232,3537,3538],{"class":1234,"line":2063},[1232,3539,3540],{"class":1303},"      \u003Chr \u002F>\n",[1232,3542,3543],{"class":1234,"line":2068},[1232,3544,1283],{"emptyLinePlaceholder":1282},[1232,3546,3547],{"class":1234,"line":2836},[1232,3548,3549],{"class":1303},"      \u003Ch3>會員類型\u003C\u002Fh3>\n",[1232,3551,3552],{"class":1234,"line":2841},[1232,3553,1283],{"emptyLinePlaceholder":1282},[1232,3555,3556],{"class":1234,"line":2846},[1232,3557,3558],{"class":1303},"      \u003Cselect\n",[1232,3560,3561],{"class":1234,"line":2851},[1232,3562,3563],{"class":1303},"        [value]=\"discount.memberType()\"\n",[1232,3565,3566],{"class":1234,"line":2856},[1232,3567,3568],{"class":1303},"        (change)=\"changeMember($event)\"\n",[1232,3570,3571],{"class":1234,"line":2871},[1232,3572,3573],{"class":1303},"      >\n",[1232,3575,3576],{"class":1234,"line":2886},[1232,3577,3578],{"class":1303},"        \u003Coption value=\"normal\">一般會員\u003C\u002Foption>\n",[1232,3580,3581],{"class":1234,"line":2891},[1232,3582,3583],{"class":1303},"        \u003Coption value=\"vip\">VIP 會員\u003C\u002Foption>\n",[1232,3585,3586],{"class":1234,"line":2896},[1232,3587,3588],{"class":1303},"        \u003Coption value=\"business\">企業會員\u003C\u002Foption>\n",[1232,3590,3591],{"class":1234,"line":2904},[1232,3592,3593],{"class":1303},"      \u003C\u002Fselect>\n",[1232,3595,3596],{"class":1234,"line":2909},[1232,3597,1283],{"emptyLinePlaceholder":1282},[1232,3599,3600],{"class":1234,"line":2914},[1232,3601,3540],{"class":1303},[1232,3603,3604],{"class":1234,"line":2931},[1232,3605,1283],{"emptyLinePlaceholder":1282},[1232,3607,3608],{"class":1234,"line":2949},[1232,3609,3610],{"class":1303},"      \u003Ch3>購物車\u003C\u002Fh3>\n",[1232,3612,3613],{"class":1234,"line":2965},[1232,3614,1283],{"emptyLinePlaceholder":1282},[1232,3616,3617],{"class":1234,"line":2984},[1232,3618,3619],{"class":1303},"      @for (item of store.items(); track item.id) {\n",[1232,3621,3622],{"class":1234,"line":3000},[1232,3623,3624],{"class":1303},"        \u003Cp>\n",[1232,3626,3627],{"class":1234,"line":3006},[1232,3628,3629],{"class":1303},"          {{ item.name }} × {{ item.quantity }}\n",[1232,3631,3632],{"class":1234,"line":3011},[1232,3633,3634],{"class":1303},"          = {{ item.price * item.quantity }} 元\n",[1232,3636,3637],{"class":1234,"line":3024},[1232,3638,1283],{"emptyLinePlaceholder":1282},[1232,3640,3641],{"class":1234,"line":3041},[1232,3642,3643],{"class":1303},"          \u003Cbutton (click)=\"store.remove(item.id)\">\n",[1232,3645,3646],{"class":1234,"line":3051},[1232,3647,3648],{"class":1303},"            移除\n",[1232,3650,3651],{"class":1234,"line":3074},[1232,3652,3653],{"class":1303},"          \u003C\u002Fbutton>\n",[1232,3655,3656],{"class":1234,"line":3083},[1232,3657,3658],{"class":1303},"        \u003C\u002Fp>\n",[1232,3660,3661],{"class":1234,"line":3089},[1232,3662,3663],{"class":1303},"      } @empty {\n",[1232,3665,3666],{"class":1234,"line":3114},[1232,3667,3668],{"class":1303},"        \u003Cp>購物車目前沒有商品\u003C\u002Fp>\n",[1232,3670,3671],{"class":1234,"line":3119},[1232,3672,3673],{"class":1303},"      }\n",[1232,3675,3676],{"class":1234,"line":3128},[1232,3677,1283],{"emptyLinePlaceholder":1282},[1232,3679,3680],{"class":1234,"line":3134},[1232,3681,3540],{"class":1303},[1232,3683,3684],{"class":1234,"line":3140},[1232,3685,1283],{"emptyLinePlaceholder":1282},[1232,3687,3688],{"class":1234,"line":3145},[1232,3689,3690],{"class":1303},"      \u003Cp>商品數量：{{ store.count() }}\u003C\u002Fp>\n",[1232,3692,3693],{"class":1234,"line":3162},[1232,3694,3695],{"class":1303},"      \u003Cp>原價：{{ store.total() }} 元\u003C\u002Fp>\n",[1232,3697,3698],{"class":1234,"line":3175},[1232,3699,3700],{"class":1303},"      \u003Ch2>折扣後：{{ store.finalTotal() }} 元\u003C\u002Fh2>\n",[1232,3702,3703],{"class":1234,"line":3197},[1232,3704,1283],{"emptyLinePlaceholder":1282},[1232,3706,3707],{"class":1234,"line":3203},[1232,3708,3709],{"class":1303},"      \u003Cbutton (click)=\"store.clear()\">\n",[1232,3711,3712],{"class":1234,"line":3208},[1232,3713,3714],{"class":1303},"        清空購物車\n",[1232,3716,3717],{"class":1234,"line":3213},[1232,3718,3495],{"class":1303},[1232,3720,3721],{"class":1234,"line":3222},[1232,3722,3723],{"class":1303},"    \u003C\u002Fmain>\n",[1232,3725,3726],{"class":1234,"line":3230},[1232,3727,3728],{"class":1303},"  `\n",[1232,3730,3731],{"class":1234,"line":3235},[1232,3732,3733],{"class":1251},"})\n",[1232,3735,3736,3738,3740,3743],{"class":1234,"line":3241},[1232,3737,1461],{"class":1237},[1232,3739,1529],{"class":1237},[1232,3741,3742],{"class":1467}," CartComponent",[1232,3744,1258],{"class":1251},[1232,3746,3748,3750,3753,3755,3757],{"class":1234,"line":3747},74,[1232,3749,1892],{"class":1237},[1232,3751,3752],{"class":1481}," store",[1232,3754,1244],{"class":1237},[1232,3756,2699],{"class":1247},[1232,3758,3759],{"class":1251},"(NgRxCartStore);\n",[1232,3761,3763,3765,3767,3769,3771],{"class":1234,"line":3762},75,[1232,3764,1892],{"class":1237},[1232,3766,2694],{"class":1481},[1232,3768,1244],{"class":1237},[1232,3770,2699],{"class":1247},[1232,3772,3773],{"class":1251},"(DiscountService);\n",[1232,3775,3777],{"class":1234,"line":3776},76,[1232,3778,1283],{"emptyLinePlaceholder":1282},[1232,3780,3782,3785,3787,3790,3792,3795],{"class":1234,"line":3781},77,[1232,3783,3784],{"class":1247},"  changeMember",[1232,3786,1478],{"class":1251},[1232,3788,3789],{"class":1481},"event",[1232,3791,1484],{"class":1237},[1232,3793,3794],{"class":1467}," Event",[1232,3796,1307],{"class":1251},[1232,3798,3800,3802,3805,3807,3810,3812,3815],{"class":1234,"line":3799},78,[1232,3801,2691],{"class":1237},[1232,3803,3804],{"class":1240}," select",[1232,3806,1244],{"class":1237},[1232,3808,3809],{"class":1251}," event.target ",[1232,3811,2653],{"class":1237},[1232,3813,3814],{"class":1467}," HTMLSelectElement",[1232,3816,1324],{"class":1251},[1232,3818,3820,3822,3825,3828,3831,3833,3835],{"class":1234,"line":3819},79,[1232,3821,2051],{"class":1240},[1232,3823,3824],{"class":1251},".discount.",[1232,3826,3827],{"class":1247},"change",[1232,3829,3830],{"class":1251},"(select.value ",[1232,3832,2653],{"class":1237},[1232,3834,1840],{"class":1467},[1232,3836,1915],{"class":1251},[1232,3838,3840],{"class":1234,"line":3839},80,[1232,3841,1329],{"class":1251},[1232,3843,3845],{"class":1234,"line":3844},81,[1232,3846,1501],{"class":1251},[1216,3848,3849,3850,3852,3853,3855,3856,3858],{},"Component 直接注入 ",[1229,3851,1742],{},"，因為需要呼叫 ",[1229,3854,2094],{}," 切換會員類型；SignalStore 則透過 ",[1229,3857,2211],{}," 使用同一個實體來計算折扣。",[1216,3860,3861],{},"假設購物車有一個 1,200 元的鍵盤，以及一個 800 元的滑鼠：",[2426,3863,3864,3877],{},[2429,3865,3866],{},[2432,3867,3868,3871,3874],{},[2435,3869,3870],{},"會員類型",[2435,3872,3873],{},"折扣",[2435,3875,3876],{},"結帳金額",[2442,3878,3879,3890,3901],{},[2432,3880,3881,3884,3887],{},[2447,3882,3883],{},"一般會員",[2447,3885,3886],{},"原價",[2447,3888,3889],{},"2,000 元",[2432,3891,3892,3895,3898],{},[2447,3893,3894],{},"VIP 會員",[2447,3896,3897],{},"9 折",[2447,3899,3900],{},"1,800 元",[2432,3902,3903,3906,3909],{},[2447,3904,3905],{},"企業會員",[2447,3907,3908],{},"85 折",[2447,3910,3911],{},"1,700 元",[1216,3913,3914],{},"切換會員類型後，購物車商品與原價都不會改變，只有折扣金額會跟著更新。",[1216,3916,3917],{},[1726,3918],{"alt":3919,"src":3920},"Runtime 切換折扣策略：切換 memberType 後選擇對應 Strategy，finalTotal 自動更新","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791640689\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2026%EF%BC%9ASignalStore%20%E8%88%87%20Strategy%20Pattern%EF%BC%8C%E9%80%8F%E9%81%8E%20DI%20%E5%8B%95%E6%85%8B%E5%88%87%E6%8F%9B%E6%A5%AD%E5%8B%99%E9%82%8F%E8%BC%AF\u002Fruntime-discount-switching.png",[3922,3923,3924],"blockquote",{},[1216,3925,3926],{},[3927,3928,3932],"a",{"href":3929,"rel":3930},"https:\u002F\u002Fstackblitz.com\u002Fedit\u002Fldbybozv-vvnkpscz?file=src%2Fapp%2Fcart%2Fdiscount.strategy.ts",[3931],"nofollow","Angular SignalStore 與 Strategy Pattern，透過 DI 動態切換業務邏輯 - StackBlitz",[1424,3934,3936],{"id":3935},"為什麼切換後會自動更新","為什麼切換後會自動更新？",[1216,3938,3939,3940,3942],{},"回頭看 SignalStore 裡面的 ",[1229,3941,2504],{},"：",[1222,3944,3946],{"className":1224,"code":3945,"language":1226,"meta":1227,"style":1227},"const finalTotal = computed(() =>\n  discount.calculate(total())\n);\n",[1229,3947,3948,3962,3974],{"__ignoreMap":1227},[1232,3949,3950,3952,3954,3956,3958,3960],{"class":1234,"line":340},[1232,3951,1029],{"class":1237},[1232,3953,1241],{"class":1240},[1232,3955,1244],{"class":1237},[1232,3957,1248],{"class":1247},[1232,3959,1252],{"class":1251},[1232,3961,2725],{"class":1237},[1232,3963,3964,3966,3968,3970,3972],{"class":1234,"line":329},[1232,3965,3290],{"class":1251},[1232,3967,2019],{"class":1247},[1232,3969,1478],{"class":1251},[1232,3971,1274],{"class":1247},[1232,3973,2883],{"class":1251},[1232,3975,3976],{"class":1234,"line":318},[1232,3977,1915],{"class":1251},[1216,3979,3980,3981,3983,3984,3986,3987,3989],{},"雖然這裡沒有直接讀取 ",[1229,3982,2081],{},"，但 ",[1229,3985,1742],{}," 的 ",[1229,3988,1708],{}," 內部會使用這個 Signal：",[1222,3991,3993],{"className":1224,"code":3992,"language":1226,"meta":1227,"style":1227},"calculate(total: number) {\n  return this.strategies[\n    this.memberType()\n  ].calculate(total);\n}\n",[1229,3994,3995,4002,4010,4020,4029],{"__ignoreMap":1227},[1232,3996,3997,3999],{"class":1234,"line":340},[1232,3998,2019],{"class":1247},[1232,4000,4001],{"class":1251},"(total: number) {\n",[1232,4003,4004,4006,4008],{"class":1234,"line":329},[1232,4005,1376],{"class":1237},[1232,4007,1995],{"class":1240},[1232,4009,1998],{"class":1251},[1232,4011,4012,4014,4016,4018],{"class":1234,"line":318},[1232,4013,2051],{"class":1240},[1232,4015,2006],{"class":1251},[1232,4017,1294],{"class":1247},[1232,4019,2011],{"class":1251},[1232,4021,4022,4025,4027],{"class":1234,"line":134},[1232,4023,4024],{"class":1251},"  ].",[1232,4026,2019],{"class":1247},[1232,4028,2022],{"class":1251},[1232,4030,4031],{"class":1234,"line":122},[1232,4032,1501],{"class":1251},[1216,4034,4035,4036,4039],{},"Angular 的 ",[1229,4037,4038],{},"computed()"," 會追蹤計算過程中讀取的 Signal，即使透過其他方法間接讀取，也一樣會建立依賴關係。",[1216,4041,4042,4043,4045,4046,4049,4050,4052],{},"因此，",[1229,4044,2504],{}," 同時依賴購物車的 ",[1229,4047,4048],{},"total()"," 與會員類型的 ",[1229,4051,2081],{},"。當其中一個改變時，就會在下次讀取時重新計算。",[1216,4054,4055,4056,4062],{},"需要注意的是，",[1392,4057,4058,4059,4061],{},"Runtime 切換的是 ",[1229,4060,1742],{}," 內部使用的 Strategy，而不是 Angular DI Provider 本身","。DI 負責提供實體，Signal 則讓折扣計算能隨著會員類型改變。",[1216,4064,4065],{},"這次為了示範，讓使用者可以直接切換會員類型。實際系統中的會員等級通常由登入資訊或後端資料決定，最終結帳金額也應由後端驗證。",[1397,4067,4068],{"id":4068},"本日小結",[1216,4070,4071,4072,4074],{},"今天透過 Strategy Pattern，將不同會員的折扣規則拆成獨立 Class，讓 SignalStore 不需要處理各種折扣判斷，只需要呼叫共同的 ",[1229,4073,1708],{}," 方法。",[1216,4076,4077,4078,4080,4081,4083,4084,2420,4086,4088],{},"Angular DI 負責建立與管理 Strategy，",[1229,4079,1742],{}," 則透過 ",[1229,4082,2087],{}," 取得各個折扣實體。搭配 ",[1229,4085,2132],{},[1229,4087,2398],{},"，即使 SignalStore 與 Component 注入不同的 Token，也能共用同一個 Service。",[1216,4090,4091,4092,4094],{},"當會員類型改變時，",[1229,4093,1742],{}," 會選擇對應的 Strategy，而 SignalStore 的 Computed 也會依照最新狀態重新計算金額，不需要重新建立 Store 或替換 DI Provider。",[1216,4096,4097],{},"這個範例將折扣規則與購物車狀態分開管理。未來需要增加新的會員折扣時，可以擴充 Strategy，讓原本的 Store 不必跟著調整。",[1424,4099,4100],{"id":4100},"資料來源",[1408,4102,4103,4116,4126,4136],{},[1411,4104,4105,3942,4110,4112,4113,4115],{},[3927,4106,4109],{"href":4107,"rel":4108},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fdi\u002Fdefining-dependency-providers#useexisting",[3931],"Angular 官方文件：useExisting",[1229,4111,2398],{}," 共用實體，",[1229,4114,2423],{}," 另建實體。",[1411,4117,4118,4123,4124,2133],{},[3927,4119,4122],{"href":4120,"rel":4121},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fdi\u002Fdefining-dependency-providers#can-typescript-interfaces-be-identifiers-for-injection",[3931],"Angular 官方文件：Can TypeScript interfaces be identifiers for injection?","：Interface 不能當 DI Token，改用 ",[1229,4125,2132],{},[1411,4127,4128,3942,4133,4135],{},[3927,4129,4132],{"href":4130,"rel":4131},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals#computed-signal-dependencies-are-dynamic",[3931],"Angular Signals 官方文件：Computed signal dependencies are dynamic",[1229,4134,4038],{}," 追蹤計算中實際讀取的 Signal。",[1411,4137,4138,3942,4143,4146,4147,2133],{},[3927,4139,4142],{"href":4140,"rel":4141},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Fsignal-store#defining-store-properties",[3931],"NgRx 官方文件：SignalStore",[1229,4144,4145],{},"withComputed()"," 在 Injection Context 執行，可使用 ",[1229,4148,2087],{},[4150,4151,4152],"style",{},"html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sMOND, html code.shiki .sMOND{--shiki-default:#6F42C1;--shiki-dark:#6CB6FF}",{"title":1227,"searchDepth":134,"depth":134,"links":4154},[4155,4158,4161,4162,4163,4166],{"id":1399,"depth":329,"text":1400,"children":4156},[4157],{"id":1426,"depth":318,"text":1426},{"id":1732,"depth":329,"text":1733,"children":4159},[4160],{"id":1746,"depth":318,"text":1747},{"id":2113,"depth":329,"text":2114},{"id":2493,"depth":329,"text":2494},{"id":3333,"depth":329,"text":3334,"children":4164},[4165],{"id":3935,"depth":318,"text":3936},{"id":4068,"depth":329,"text":4068,"children":4167},[4168],{"id":4100,"depth":318,"text":4100},[11,13],"md",{},"---\ntitle: \"Day 26：SignalStore 與 Strategy Pattern，透過 DI 動態切換業務邏輯\"\ndate: 2026-10-10 22:01:59\ntags:\n  - \"iThome 鐵人賽\"\n  - \"2026 鐵人賽\"\n  - \"Angular\"\ncategories:\n  - \"iThome 鐵人賽\"\n  - \"Angular\"\ndescription: \"昨天我們使用 Angular 原生 Signal 與 NgRx SignalStore 實作購物車，透過 State 管理商品資料，再利用 Computed 計算商品數量與總金額。\"\ncanonical: https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423116\nseries:\n  name: \"2026 iThome 鐵人賽：Angular 22 Signal 進化論\"\n  url: https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F9939\n  day: 26\n---\n\n昨天我們使用 Angular 原生 Signal 與 NgRx SignalStore 實作購物車，透過 State 管理商品資料，再利用 Computed 計算商品數量與總金額。\n\n不過，當購物車需要根據不同會員套用折扣時，可能會直接在 Store 裡面增加判斷：\n\n```ts\nconst finalTotal = computed(() => {\n  const total = store.total();\n\n  if (memberType() === 'vip') {\n    return total * 0.9;\n  }\n\n  if (memberType() === 'business') {\n    return total * 0.85;\n  }\n\n  return total;\n});\n```\n\n目前只有三種會員，這樣寫沒有什麼問題。但如果未來增加更多折扣規則，Store 就需要不斷修改。\n\n今天我們會使用 **Strategy Pattern（策略模式）** 拆分折扣計算，再搭配 Angular Dependency Injection（DI）與 Signal，讓會員類型改變時，可以動態切換對應的折扣規則。\n\n## Strategy Pattern 是什麼？\n\nStrategy Pattern 是一種行為型設計模式，主要是將不同的演算法或業務規則封裝成獨立物件，讓使用端透過共同介面操作。\n\n以購物車為例，假設有三種會員：\n\n- 一般會員：原價。\n- VIP 會員：9 折。\n- 企業會員：85 折。\n\n三種會員的計算方式不同，但都需要根據總金額計算折扣，因此可以先定義共同介面。\n\n### 定義折扣策略\n\n`discount.strategy.ts`\n\n```ts\nimport { Injectable } from '@angular\u002Fcore';\n\nexport interface DiscountStrategy {\n  calculate(total: number): number;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class NormalDiscount implements DiscountStrategy {\n  calculate(total: number) {\n    return total;\n  }\n}\n\n@Injectable({ providedIn: 'root' })\nexport class VipDiscount implements DiscountStrategy {\n  calculate(total: number) {\n    return total * 0.9;\n  }\n}\n\n@Injectable({ providedIn: 'root' })\nexport class BusinessDiscount implements DiscountStrategy {\n  calculate(total: number) {\n    return total * 0.85;\n  }\n}\n```\n\n每個 Class 都實作 `DiscountStrategy`，提供相同的 `calculate()` 方法，但使用不同的折扣規則。未來新增其他折扣時，只需要建立新的 Strategy，不必將所有判斷集中在 Store 裡。\n\n這裡透過 `@Injectable({ providedIn: 'root' })` 將三個 Strategy 註冊到 Angular DI。它們通常會在第一次被注入時才建立實體，之後由 Root Injector 共用，而不是 Application 啟動時就全部建立。\n\n下圖整理三個 Strategy 與共同介面的關係，無論使用哪種折扣規則，都能透過相同的 `calculate()` 方法計算。\n\n![Strategy Pattern 折扣策略：三個 Strategy 實作同一個 DiscountStrategy 介面](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791640682\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2026%EF%BC%9ASignalStore%20%E8%88%87%20Strategy%20Pattern%EF%BC%8C%E9%80%8F%E9%81%8E%20DI%20%E5%8B%95%E6%85%8B%E5%88%87%E6%8F%9B%E6%A5%AD%E5%8B%99%E9%82%8F%E8%BC%AF\u002Fstrategy-pattern-discounts.png)\n\n## 使用 Service 動態切換 Strategy\n\n將折扣規則拆開後，還需要根據目前的會員類型，選擇要使用哪一種 Strategy。\n\n這次建立 `DiscountService`，透過 Signal 記錄會員類型，並利用 Angular DI 取得三個折扣策略。\n\n### 建立 DiscountService\n\n`discount.service.ts`\n\n```ts\nimport { Injectable, inject, signal } from '@angular\u002Fcore';\n\nimport {\n  NormalDiscount,\n  VipDiscount,\n  BusinessDiscount\n} from '.\u002Fdiscount.strategy';\n\nimport type { DiscountStrategy } from '.\u002Fdiscount.strategy';\n\nexport type MemberType = 'normal' | 'vip' | 'business';\n\n@Injectable({ providedIn: 'root' })\nexport class DiscountService implements DiscountStrategy {\n  readonly memberType = signal\u003CMemberType>('normal');\n\n  private readonly strategies = {\n    normal: inject(NormalDiscount),\n    vip: inject(VipDiscount),\n    business: inject(BusinessDiscount)\n  };\n\n  calculate(total: number) {\n    return this.strategies[\n      this.memberType()\n    ].calculate(total);\n  }\n\n  change(type: MemberType) {\n    this.memberType.set(type);\n  }\n}\n```\n\n`DiscountService` 本身也實作了 `DiscountStrategy`，但不直接計算折扣，而是根據 `memberType()` 選擇對應的 Strategy。\n\n這裡透過 `inject()` 取得三個 Strategy，因此當 `DiscountService` 第一次建立時，就會一併取得這些實體。之後呼叫 `change()` 切換會員類型時，只需要改用對應的 Strategy，不需要重新建立。\n\n例如目前是 VIP 會員，就使用 `VipDiscount`；當呼叫 `change('business')` 後，便改由 `BusinessDiscount` 計算。\n\n## 透過 Angular DI 提供 Strategy\n\n有了 `DiscountService` 後，我們希望 SignalStore 只需要使用 `calculate()`，不必直接依賴 `DiscountService` 這個 Class。\n\n由於 TypeScript Interface 在編譯後不會保留，無法直接作為 Angular DI 的 Token，因此需要建立 `InjectionToken`。\n\n`discount.token.ts`\n\n```ts\nimport { InjectionToken } from '@angular\u002Fcore';\nimport type { DiscountStrategy } from '.\u002Fdiscount.strategy';\n\nexport const DISCOUNT_STRATEGY =\n  new InjectionToken\u003CDiscountStrategy>('DISCOUNT_STRATEGY');\n```\n\n`DISCOUNT_STRATEGY` 是注入時使用的 Token，泛型則指定取得的物件需要符合 `DiscountStrategy` 介面。\n\n接著在 `app.config.ts` 註冊：\n\n```ts\nimport { ApplicationConfig } from '@angular\u002Fcore';\nimport { provideRouter } from '@angular\u002Frouter';\nimport { provideHttpClient } from '@angular\u002Fcommon\u002Fhttp';\n\nimport { routes } from '.\u002Fapp.routes';\nimport { DISCOUNT_STRATEGY } from '.\u002Fcart\u002Fdiscount.token';\nimport { DiscountService } from '.\u002Fcart\u002Fdiscount.service';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideRouter(routes),\n    provideHttpClient(),\n    {\n      provide: DISCOUNT_STRATEGY,\n      useExisting: DiscountService\n    }\n  ]\n};\n```\n\n這裡使用 `useExisting`，讓 `DISCOUNT_STRATEGY` 成為 `DiscountService` 的另一個注入名稱。由於 `DiscountService` 已經透過 `providedIn: 'root'` 註冊，因此不需要再加入 `providers`。\n\n`useExisting` 與 `useClass` 的差別在於是否共用實體：\n\n| Provider | 說明 |\n| --- | --- |\n| `useClass` | 由 DI 為該 Token 建立指定 Class 的實體 |\n| `useExisting` | 使用另一個 Token 對應的同一個實體 |\n\n如果改成 `useClass: DiscountService`，Angular 就會為 `DISCOUNT_STRATEGY` 建立另一個 `DiscountService` 實體，與原本透過 Root 註冊的實體分開管理。\n\n這次使用 `useExisting`，是因為 Component 需要切換會員類型，而 SignalStore 需要根據相同的會員狀態計算折扣，因此兩者必須取得同一個 `DiscountService` 實體。\n\n![Angular DI useExisting：CartComponent 與 NgRxCartStore 共用同一個 DiscountService 實體](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791640685\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2026%EF%BC%9ASignalStore%20%E8%88%87%20Strategy%20Pattern%EF%BC%8C%E9%80%8F%E9%81%8E%20DI%20%E5%8B%95%E6%85%8B%E5%88%87%E6%8F%9B%E6%A5%AD%E5%8B%99%E9%82%8F%E8%BC%AF\u002Fdi-use-existing.png)\n\n## 在 SignalStore 使用 Strategy\n\n延續昨天的 `NgRxCartStore`，保留原本的購物車功能，另外增加折扣後的 `finalTotal`。\n\n`ngrx-cart.store.ts`\n\n```ts\nimport { computed, inject } from '@angular\u002Fcore';\nimport {\n  signalStore,\n  withState,\n  withComputed,\n  withMethods,\n  patchState\n} from '@ngrx\u002Fsignals';\n\nimport type { CartItem, Product } from '.\u002Fcart.types';\nimport { DISCOUNT_STRATEGY } from '.\u002Fdiscount.token';\n\nexport const NgRxCartStore = signalStore(\n  { providedIn: 'root' },\n\n  withState({\n    items: [] as CartItem[]\n  }),\n\n  withComputed(({ items }) => {\n    const discount = inject(DISCOUNT_STRATEGY);\n\n    const count = computed(() =>\n      items().reduce(\n        (sum, item) => sum + item.quantity,\n        0\n      )\n    );\n\n    const total = computed(() =>\n      items().reduce(\n        (sum, item) => sum + item.price * item.quantity,\n        0\n      )\n    );\n\n    const finalTotal = computed(() =>\n      discount.calculate(total())\n    );\n\n    return { count, total, finalTotal };\n  }),\n\n  withMethods(store => ({\n    add(product: Product) {\n      patchState(store, state => {\n        const existing = state.items.find(\n          item => item.id === product.id\n        );\n\n        const items = existing\n          ? state.items.map(item =>\n              item.id === product.id\n                ? { ...item, quantity: item.quantity + 1 }\n                : item\n            )\n          : [...state.items, { ...product, quantity: 1 }];\n\n        return { items };\n      });\n    },\n\n    remove(id: number) {\n      patchState(store, state => ({\n        items: state.items.filter(item => item.id !== id)\n      }));\n    },\n\n    clear() {\n      patchState(store, { items: [] });\n    }\n  }))\n);\n```\n\n這次主要新增：\n\n```ts\nconst discount = inject(DISCOUNT_STRATEGY);\n\nconst finalTotal = computed(() =>\n  discount.calculate(total())\n);\n```\n\n`inject(DISCOUNT_STRATEGY)` 會透過 Angular DI 取得前面註冊的 `DiscountService` 實體，再由 `calculate()` 計算折扣後的金額。\n\n這裡也可以直接使用 `inject(DiscountService)`，但透過 `DISCOUNT_STRATEGY`，Store 只需要知道 `calculate()`，不必依賴 `DiscountService` 的其他功能。未來如果更換成其他符合介面的折扣服務，也不需要修改 Store。\n\n## Component 切換會員類型\n\n最後在 Component 加入會員選單，讓使用者可以切換折扣。\n\n`cart.ts`\n\n```ts\nimport { Component, inject } from '@angular\u002Fcore';\n\nimport { NgRxCartStore } from '.\u002Fngrx-cart.store';\nimport { DiscountService } from '.\u002Fdiscount.service';\nimport type { MemberType } from '.\u002Fdiscount.service';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003Cmain style=\"padding: 24px; max-width: 600px;\">\n      \u003Ch1>Strategy Pattern 購物車\u003C\u002Fh1>\n\n      \u003Ch3>商品列表\u003C\u002Fh3>\n\n      \u003Cbutton (click)=\"store.add({\n        id: 1,\n        name: '鍵盤',\n        price: 1200\n      })\">\n        加入鍵盤 $1200\n      \u003C\u002Fbutton>\n\n      \u003Cbutton (click)=\"store.add({\n        id: 2,\n        name: '滑鼠',\n        price: 800\n      })\">\n        加入滑鼠 $800\n      \u003C\u002Fbutton>\n\n      \u003Chr \u002F>\n\n      \u003Ch3>會員類型\u003C\u002Fh3>\n\n      \u003Cselect\n        [value]=\"discount.memberType()\"\n        (change)=\"changeMember($event)\"\n      >\n        \u003Coption value=\"normal\">一般會員\u003C\u002Foption>\n        \u003Coption value=\"vip\">VIP 會員\u003C\u002Foption>\n        \u003Coption value=\"business\">企業會員\u003C\u002Foption>\n      \u003C\u002Fselect>\n\n      \u003Chr \u002F>\n\n      \u003Ch3>購物車\u003C\u002Fh3>\n\n      @for (item of store.items(); track item.id) {\n        \u003Cp>\n          {{ item.name }} × {{ item.quantity }}\n          = {{ item.price * item.quantity }} 元\n\n          \u003Cbutton (click)=\"store.remove(item.id)\">\n            移除\n          \u003C\u002Fbutton>\n        \u003C\u002Fp>\n      } @empty {\n        \u003Cp>購物車目前沒有商品\u003C\u002Fp>\n      }\n\n      \u003Chr \u002F>\n\n      \u003Cp>商品數量：{{ store.count() }}\u003C\u002Fp>\n      \u003Cp>原價：{{ store.total() }} 元\u003C\u002Fp>\n      \u003Ch2>折扣後：{{ store.finalTotal() }} 元\u003C\u002Fh2>\n\n      \u003Cbutton (click)=\"store.clear()\">\n        清空購物車\n      \u003C\u002Fbutton>\n    \u003C\u002Fmain>\n  `\n})\nexport class CartComponent {\n  readonly store = inject(NgRxCartStore);\n  readonly discount = inject(DiscountService);\n\n  changeMember(event: Event) {\n    const select = event.target as HTMLSelectElement;\n    this.discount.change(select.value as MemberType);\n  }\n}\n```\n\nComponent 直接注入 `DiscountService`，因為需要呼叫 `change()` 切換會員類型；SignalStore 則透過 `DISCOUNT_STRATEGY` 使用同一個實體來計算折扣。\n\n假設購物車有一個 1,200 元的鍵盤，以及一個 800 元的滑鼠：\n\n| 會員類型 | 折扣 | 結帳金額 |\n| --- | --- | --- |\n| 一般會員 | 原價 | 2,000 元 |\n| VIP 會員 | 9 折 | 1,800 元 |\n| 企業會員 | 85 折 | 1,700 元 |\n\n切換會員類型後，購物車商品與原價都不會改變，只有折扣金額會跟著更新。\n\n![Runtime 切換折扣策略：切換 memberType 後選擇對應 Strategy，finalTotal 自動更新](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791640689\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2026%EF%BC%9ASignalStore%20%E8%88%87%20Strategy%20Pattern%EF%BC%8C%E9%80%8F%E9%81%8E%20DI%20%E5%8B%95%E6%85%8B%E5%88%87%E6%8F%9B%E6%A5%AD%E5%8B%99%E9%82%8F%E8%BC%AF\u002Fruntime-discount-switching.png)\n\n> [Angular SignalStore 與 Strategy Pattern，透過 DI 動態切換業務邏輯 - StackBlitz](https:\u002F\u002Fstackblitz.com\u002Fedit\u002Fldbybozv-vvnkpscz?file=src%2Fapp%2Fcart%2Fdiscount.strategy.ts)\n\n### 為什麼切換後會自動更新？\n\n回頭看 SignalStore 裡面的 `finalTotal`：\n\n```ts\nconst finalTotal = computed(() =>\n  discount.calculate(total())\n);\n```\n\n雖然這裡沒有直接讀取 `memberType()`，但 `DiscountService` 的 `calculate()` 內部會使用這個 Signal：\n\n```ts\ncalculate(total: number) {\n  return this.strategies[\n    this.memberType()\n  ].calculate(total);\n}\n```\n\nAngular 的 `computed()` 會追蹤計算過程中讀取的 Signal，即使透過其他方法間接讀取，也一樣會建立依賴關係。\n\n因此，`finalTotal` 同時依賴購物車的 `total()` 與會員類型的 `memberType()`。當其中一個改變時，就會在下次讀取時重新計算。\n\n需要注意的是，**Runtime 切換的是 `DiscountService` 內部使用的 Strategy，而不是 Angular DI Provider 本身**。DI 負責提供實體，Signal 則讓折扣計算能隨著會員類型改變。\n\n這次為了示範，讓使用者可以直接切換會員類型。實際系統中的會員等級通常由登入資訊或後端資料決定，最終結帳金額也應由後端驗證。\n\n## 本日小結\n\n今天透過 Strategy Pattern，將不同會員的折扣規則拆成獨立 Class，讓 SignalStore 不需要處理各種折扣判斷，只需要呼叫共同的 `calculate()` 方法。\n\nAngular DI 負責建立與管理 Strategy，`DiscountService` 則透過 `inject()` 取得各個折扣實體。搭配 `InjectionToken` 與 `useExisting`，即使 SignalStore 與 Component 注入不同的 Token，也能共用同一個 Service。\n\n當會員類型改變時，`DiscountService` 會選擇對應的 Strategy，而 SignalStore 的 Computed 也會依照最新狀態重新計算金額，不需要重新建立 Store 或替換 DI Provider。\n\n這個範例將折扣規則與購物車狀態分開管理。未來需要增加新的會員折扣時，可以擴充 Strategy，讓原本的 Store 不必跟著調整。\n\n### 資料來源\n\n- [Angular 官方文件：useExisting](https:\u002F\u002Fangular.dev\u002Fguide\u002Fdi\u002Fdefining-dependency-providers#useexisting)：`useExisting` 共用實體，`useClass` 另建實體。\n- [Angular 官方文件：Can TypeScript interfaces be identifiers for injection?](https:\u002F\u002Fangular.dev\u002Fguide\u002Fdi\u002Fdefining-dependency-providers#can-typescript-interfaces-be-identifiers-for-injection)：Interface 不能當 DI Token，改用 `InjectionToken`。\n- [Angular Signals 官方文件：Computed signal dependencies are dynamic](https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals#computed-signal-dependencies-are-dynamic)：`computed()` 追蹤計算中實際讀取的 Signal。\n- [NgRx 官方文件：SignalStore](https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Fsignal-store#defining-store-properties)：`withComputed()` 在 Injection Context 執行，可使用 `inject()`。\n",{"title":59,"description":60},{"name":20,"url":21,"day":67},{"loc":58,"images":4176,"lastmod":61},[4177,4178,4179],{"loc":1729},{"loc":2490},{"loc":3920},"ithome-2026-day-26","2026-10-10-ithome-2026-day-26",[11,12,13],"7daLKEEZWaN2VqxntOddYWeVjlTkZcI1pNyRPzCVZpA",1792575170104]