[{"data":1,"prerenderedAt":4864},["ShallowReactive",2],{"all-posts":3,"post:\u002Fithome-2026-day-25":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":70,"body":1212,"canonical":77,"categories":4850,"date":72,"description":71,"draft":15,"extension":4851,"meta":4852,"navigation":1382,"ogImage":9,"path":69,"rawbody":4853,"readingTime":76,"robots":9,"schemaOrg":9,"seo":4854,"series":4855,"sitemap":4856,"slug":4860,"stem":4861,"tags":4862,"updated":9,"wordCount":75,"__hash__":4863},"posts\u002F2026-10-09-ithome-2026-day-25.md",{"type":1213,"value":1214,"toc":4832},"minimark",[1215,1224,1227,1241,1244,1249,1252,1255,1278,1286,1293,1298,1301,1306,1416,1426,1431,2019,2028,2031,2071,2094,2100,2117,2121,2128,2133,2344,2347,2352,2458,2461,2466,2625,2635,2638,2641,2645,2648,2651,2654,2657,2660,2664,2667,2737,2740,2745,3322,3325,3337,3348,3360,3365,3381,3387,3391,3394,3498,3505,3508,3512,3515,3518,3642,3645,3649,3652,3709,3715,3909,3920,3926,3932,4068,4076,4086,4161,4171,4175,4181,4184,4434,4444,4447,4473,4478,4484,4653,4664,4714,4723,4736,4742,4746,4749,4752,4755,4758,4761,4774,4783,4786,4789,4828],[1216,1217,1218,1219,1223],"p",{},"在 Angular 開發中，如果資料只會被單一 Component 使用，通常可以直接透過 ",[1220,1221,1222],"code",{},"signal()"," 管理。但當多個 Component 需要共用同一份資料時，就得考慮狀態應該放在哪裡。",[1216,1225,1226],{},"例如電商網站的購物車，商品列表需要新增商品、Navbar 需要顯示購物車數量，購物車頁面則需要計算總金額。如果每個 Component 都管理自己的資料，就得額外處理彼此之間的同步。",[1216,1228,1229,1230,1233,1234,1236,1237,1240],{},"過去我們可能會使用 RxJS ",[1220,1231,1232],{},"BehaviorSubject","，或引入 NgRx 這類狀態管理工具。但現在 Angular 已經提供 ",[1220,1235,1222],{},"、",[1220,1238,1239],{},"computed()"," 與 Dependency Injection（DI），是不是也能直接用這些功能建立 Store？",[1216,1242,1243],{},"這篇會先使用原生 Signal 實作購物車，再改用 NgRx SignalStore 完成相同功能，看看兩種寫法有什麼差別。",[1245,1246,1248],"h2",{"id":1247},"使用-signal-建立自己的-store","使用 Signal 建立自己的 Store",[1216,1250,1251],{},"Store 不一定要透過 Library 才能實作，可以先把它理解成一個集中管理狀態的地方。",[1216,1253,1254],{},"以購物車為例，Store 需要管理三種內容：",[1256,1257,1258,1266,1272],"ul",{},[1259,1260,1261,1265],"li",{},[1262,1263,1264],"strong",{},"State","：保存購物車中的商品。",[1259,1267,1268,1271],{},[1262,1269,1270],{},"Derived State","：根據商品清單計算數量與總金額。",[1259,1273,1274,1277],{},[1262,1275,1276],{},"Methods","：提供新增、刪除與清空購物車的方法。",[1216,1279,1280,1281,1236,1283,1285],{},"使用 Angular 原生的 ",[1220,1282,1222],{},[1220,1284,1239],{}," 與 Service，就能完成這些功能。",[1216,1287,1288],{},[1289,1290],"img",{"alt":1291,"src":1292},"跨 Component 共用狀態：ProductList、Navbar、Cart 共用同一個 CartStore","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791552129\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2025%EF%BC%9AAngular%20SignalStore%EF%BC%8C%E5%8E%9F%E7%94%9F%20Signal%20%E4%B8%8D%E5%A4%A0%E7%94%A8%E5%97%8E%EF%BC%9F\u002Fshared-cart-store.png",[1294,1295,1297],"h3",{"id":1296},"建立-cart-store","建立 Cart Store",[1216,1299,1300],{},"先定義商品資料結構：",[1216,1302,1303],{},[1220,1304,1305],{},"cart.types.ts",[1307,1308,1313],"pre",{"className":1309,"code":1310,"language":1311,"meta":1312,"style":1312},"language-ts shiki shiki-themes github-light github-dark-dimmed","export interface Product {\n  id: number;\n  name: string;\n  price: number;\n}\n\nexport interface CartItem extends Product {\n  quantity: number;\n}\n","ts","",[1220,1314,1315,1334,1350,1362,1373,1378,1384,1401,1412],{"__ignoreMap":1312},[1316,1317,1319,1323,1326,1330],"span",{"class":1318,"line":340},"line",[1316,1320,1322],{"class":1321},"s7YZ4","export",[1316,1324,1325],{"class":1321}," interface",[1316,1327,1329],{"class":1328},"sOLd2"," Product",[1316,1331,1333],{"class":1332},"sYgZi"," {\n",[1316,1335,1336,1340,1343,1347],{"class":1318,"line":329},[1316,1337,1339],{"class":1338},"stnAF","  id",[1316,1341,1342],{"class":1321},":",[1316,1344,1346],{"class":1345},"sviXB"," number",[1316,1348,1349],{"class":1332},";\n",[1316,1351,1352,1355,1357,1360],{"class":1318,"line":318},[1316,1353,1354],{"class":1338},"  name",[1316,1356,1342],{"class":1321},[1316,1358,1359],{"class":1345}," string",[1316,1361,1349],{"class":1332},[1316,1363,1364,1367,1369,1371],{"class":1318,"line":134},[1316,1365,1366],{"class":1338},"  price",[1316,1368,1342],{"class":1321},[1316,1370,1346],{"class":1345},[1316,1372,1349],{"class":1332},[1316,1374,1375],{"class":1318,"line":122},[1316,1376,1377],{"class":1332},"}\n",[1316,1379,1380],{"class":1318,"line":31},[1316,1381,1383],{"emptyLinePlaceholder":1382},true,"\n",[1316,1385,1386,1388,1390,1393,1396,1399],{"class":1318,"line":17},[1316,1387,1322],{"class":1321},[1316,1389,1325],{"class":1321},[1316,1391,1392],{"class":1328}," CartItem",[1316,1394,1395],{"class":1321}," extends",[1316,1397,1329],{"class":1398},"sMOND",[1316,1400,1333],{"class":1332},[1316,1402,1403,1406,1408,1410],{"class":1318,"line":110},[1316,1404,1405],{"class":1338},"  quantity",[1316,1407,1342],{"class":1321},[1316,1409,1346],{"class":1345},[1316,1411,1349],{"class":1332},[1316,1413,1414],{"class":1318,"line":76},[1316,1415,1377],{"class":1332},[1216,1417,1418,1421,1422,1425],{},[1220,1419,1420],{},"CartItem"," 除了商品資訊，還會透過 ",[1220,1423,1424],{},"quantity"," 記錄購買數量。",[1216,1427,1428],{},[1220,1429,1430],{},"cart.store.ts",[1307,1432,1434],{"className":1309,"code":1433,"language":1311,"meta":1312,"style":1312},"import { Injectable, signal, computed } from '@angular\u002Fcore';\nimport { CartItem, Product } from '.\u002Fcart.types';\n\n@Injectable({ providedIn: 'root' })\nexport class CartStore {\n  private readonly _items = signal\u003CCartItem[]>([]);\n\n  readonly items = this._items.asReadonly();\n\n  readonly count = computed(() =>\n    this.items().reduce(\n      (sum, item) => sum + item.quantity,\n      0\n    )\n  );\n\n  readonly total = computed(() =>\n    this.items().reduce(\n      (sum, item) => sum + item.price * item.quantity,\n      0\n    )\n  );\n\n  add(product: Product) {\n    this._items.update(items => {\n      const existing = items.find(\n        item => item.id === product.id\n      );\n\n      if (existing) {\n        return items.map(item =>\n          item.id === product.id\n            ? { ...item, quantity: item.quantity + 1 }\n            : item\n        );\n      }\n\n      return [...items, { ...product, quantity: 1 }];\n    });\n  }\n\n  remove(id: number) {\n    this._items.update(items =>\n      items.filter(item => item.id !== id)\n    );\n  }\n\n  clear() {\n    this._items.set([]);\n  }\n}\n",[1220,1435,1436,1453,1467,1471,1489,1501,1526,1530,1552,1556,1574,1593,1622,1627,1632,1637,1641,1656,1670,1696,1700,1704,1708,1712,1730,1748,1766,1782,1787,1791,1799,1816,1826,1849,1858,1864,1870,1875,1900,1906,1912,1917,1934,1949,1971,1977,1982,1987,1996,2009,2014],{"__ignoreMap":1312},[1316,1437,1438,1441,1444,1447,1451],{"class":1318,"line":340},[1316,1439,1440],{"class":1321},"import",[1316,1442,1443],{"class":1332}," { Injectable, signal, computed } ",[1316,1445,1446],{"class":1321},"from",[1316,1448,1450],{"class":1449},"s-HuK"," '@angular\u002Fcore'",[1316,1452,1349],{"class":1332},[1316,1454,1455,1457,1460,1462,1465],{"class":1318,"line":329},[1316,1456,1440],{"class":1321},[1316,1458,1459],{"class":1332}," { CartItem, Product } ",[1316,1461,1446],{"class":1321},[1316,1463,1464],{"class":1449}," '.\u002Fcart.types'",[1316,1466,1349],{"class":1332},[1316,1468,1469],{"class":1318,"line":318},[1316,1470,1383],{"emptyLinePlaceholder":1382},[1316,1472,1473,1476,1480,1483,1486],{"class":1318,"line":134},[1316,1474,1475],{"class":1332},"@",[1316,1477,1479],{"class":1478},"sPO5f","Injectable",[1316,1481,1482],{"class":1332},"({ providedIn: ",[1316,1484,1485],{"class":1449},"'root'",[1316,1487,1488],{"class":1332}," })\n",[1316,1490,1491,1493,1496,1499],{"class":1318,"line":122},[1316,1492,1322],{"class":1321},[1316,1494,1495],{"class":1321}," class",[1316,1497,1498],{"class":1328}," CartStore",[1316,1500,1333],{"class":1332},[1316,1502,1503,1506,1509,1512,1515,1518,1521,1523],{"class":1318,"line":31},[1316,1504,1505],{"class":1321},"  private",[1316,1507,1508],{"class":1321}," readonly",[1316,1510,1511],{"class":1338}," _items",[1316,1513,1514],{"class":1321}," =",[1316,1516,1517],{"class":1478}," signal",[1316,1519,1520],{"class":1332},"\u003C",[1316,1522,1420],{"class":1328},[1316,1524,1525],{"class":1332},"[]>([]);\n",[1316,1527,1528],{"class":1318,"line":17},[1316,1529,1383],{"emptyLinePlaceholder":1382},[1316,1531,1532,1535,1538,1540,1543,1546,1549],{"class":1318,"line":110},[1316,1533,1534],{"class":1321},"  readonly",[1316,1536,1537],{"class":1338}," items",[1316,1539,1514],{"class":1321},[1316,1541,1542],{"class":1345}," this",[1316,1544,1545],{"class":1332},"._items.",[1316,1547,1548],{"class":1478},"asReadonly",[1316,1550,1551],{"class":1332},"();\n",[1316,1553,1554],{"class":1318,"line":76},[1316,1555,1383],{"emptyLinePlaceholder":1382},[1316,1557,1558,1560,1563,1565,1568,1571],{"class":1318,"line":247},[1316,1559,1534],{"class":1321},[1316,1561,1562],{"class":1338}," count",[1316,1564,1514],{"class":1321},[1316,1566,1567],{"class":1478}," computed",[1316,1569,1570],{"class":1332},"(() ",[1316,1572,1573],{"class":1321},"=>\n",[1316,1575,1576,1579,1582,1585,1588,1590],{"class":1318,"line":236},[1316,1577,1578],{"class":1345},"    this",[1316,1580,1581],{"class":1332},".",[1316,1583,1584],{"class":1478},"items",[1316,1586,1587],{"class":1332},"().",[1316,1589,1045],{"class":1478},[1316,1591,1592],{"class":1332},"(\n",[1316,1594,1595,1598,1601,1604,1607,1610,1613,1616,1619],{"class":1318,"line":225},[1316,1596,1597],{"class":1332},"      (",[1316,1599,1600],{"class":1338},"sum",[1316,1602,1603],{"class":1332},", ",[1316,1605,1606],{"class":1338},"item",[1316,1608,1609],{"class":1332},") ",[1316,1611,1612],{"class":1321},"=>",[1316,1614,1615],{"class":1332}," sum ",[1316,1617,1618],{"class":1321},"+",[1316,1620,1621],{"class":1332}," item.quantity,\n",[1316,1623,1624],{"class":1318,"line":214},[1316,1625,1626],{"class":1345},"      0\n",[1316,1628,1629],{"class":1318,"line":203},[1316,1630,1631],{"class":1332},"    )\n",[1316,1633,1634],{"class":1318,"line":192},[1316,1635,1636],{"class":1332},"  );\n",[1316,1638,1639],{"class":1318,"line":181},[1316,1640,1383],{"emptyLinePlaceholder":1382},[1316,1642,1643,1645,1648,1650,1652,1654],{"class":1318,"line":170},[1316,1644,1534],{"class":1321},[1316,1646,1647],{"class":1338}," total",[1316,1649,1514],{"class":1321},[1316,1651,1567],{"class":1478},[1316,1653,1570],{"class":1332},[1316,1655,1573],{"class":1321},[1316,1657,1658,1660,1662,1664,1666,1668],{"class":1318,"line":159},[1316,1659,1578],{"class":1345},[1316,1661,1581],{"class":1332},[1316,1663,1584],{"class":1478},[1316,1665,1587],{"class":1332},[1316,1667,1045],{"class":1478},[1316,1669,1592],{"class":1332},[1316,1671,1672,1674,1676,1678,1680,1682,1684,1686,1688,1691,1694],{"class":1318,"line":148},[1316,1673,1597],{"class":1332},[1316,1675,1600],{"class":1338},[1316,1677,1603],{"class":1332},[1316,1679,1606],{"class":1338},[1316,1681,1609],{"class":1332},[1316,1683,1612],{"class":1321},[1316,1685,1615],{"class":1332},[1316,1687,1618],{"class":1321},[1316,1689,1690],{"class":1332}," item.price ",[1316,1692,1693],{"class":1321},"*",[1316,1695,1621],{"class":1332},[1316,1697,1698],{"class":1318,"line":137},[1316,1699,1626],{"class":1345},[1316,1701,1702],{"class":1318,"line":125},[1316,1703,1631],{"class":1332},[1316,1705,1706],{"class":1318,"line":113},[1316,1707,1636],{"class":1332},[1316,1709,1710],{"class":1318,"line":101},[1316,1711,1383],{"emptyLinePlaceholder":1382},[1316,1713,1714,1717,1720,1723,1725,1727],{"class":1318,"line":90},[1316,1715,1716],{"class":1478},"  add",[1316,1718,1719],{"class":1332},"(",[1316,1721,1722],{"class":1338},"product",[1316,1724,1342],{"class":1321},[1316,1726,1329],{"class":1328},[1316,1728,1729],{"class":1332},") {\n",[1316,1731,1732,1734,1736,1739,1741,1743,1746],{"class":1318,"line":79},[1316,1733,1578],{"class":1345},[1316,1735,1545],{"class":1332},[1316,1737,1738],{"class":1478},"update",[1316,1740,1719],{"class":1332},[1316,1742,1584],{"class":1338},[1316,1744,1745],{"class":1321}," =>",[1316,1747,1333],{"class":1332},[1316,1749,1750,1753,1756,1758,1761,1764],{"class":1318,"line":67},[1316,1751,1752],{"class":1321},"      const",[1316,1754,1755],{"class":1345}," existing",[1316,1757,1514],{"class":1321},[1316,1759,1760],{"class":1332}," items.",[1316,1762,1763],{"class":1478},"find",[1316,1765,1592],{"class":1332},[1316,1767,1768,1771,1773,1776,1779],{"class":1318,"line":56},[1316,1769,1770],{"class":1338},"        item",[1316,1772,1745],{"class":1321},[1316,1774,1775],{"class":1332}," item.id ",[1316,1777,1778],{"class":1321},"===",[1316,1780,1781],{"class":1332}," product.id\n",[1316,1783,1784],{"class":1318,"line":45},[1316,1785,1786],{"class":1332},"      );\n",[1316,1788,1789],{"class":1318,"line":34},[1316,1790,1383],{"emptyLinePlaceholder":1382},[1316,1792,1793,1796],{"class":1318,"line":22},[1316,1794,1795],{"class":1321},"      if",[1316,1797,1798],{"class":1332}," (existing) {\n",[1316,1800,1802,1805,1807,1809,1811,1813],{"class":1318,"line":1801},31,[1316,1803,1804],{"class":1321},"        return",[1316,1806,1760],{"class":1332},[1316,1808,1043],{"class":1478},[1316,1810,1719],{"class":1332},[1316,1812,1606],{"class":1338},[1316,1814,1815],{"class":1321}," =>\n",[1316,1817,1819,1822,1824],{"class":1318,"line":1818},32,[1316,1820,1821],{"class":1332},"          item.id ",[1316,1823,1778],{"class":1321},[1316,1825,1781],{"class":1332},[1316,1827,1829,1832,1835,1838,1841,1843,1846],{"class":1318,"line":1828},33,[1316,1830,1831],{"class":1321},"            ?",[1316,1833,1834],{"class":1332}," { ",[1316,1836,1837],{"class":1321},"...",[1316,1839,1840],{"class":1332},"item, quantity: item.quantity ",[1316,1842,1618],{"class":1321},[1316,1844,1845],{"class":1345}," 1",[1316,1847,1848],{"class":1332}," }\n",[1316,1850,1852,1855],{"class":1318,"line":1851},34,[1316,1853,1854],{"class":1321},"            :",[1316,1856,1857],{"class":1332}," item\n",[1316,1859,1861],{"class":1318,"line":1860},35,[1316,1862,1863],{"class":1332},"        );\n",[1316,1865,1867],{"class":1318,"line":1866},36,[1316,1868,1869],{"class":1332},"      }\n",[1316,1871,1873],{"class":1318,"line":1872},37,[1316,1874,1383],{"emptyLinePlaceholder":1382},[1316,1876,1878,1881,1884,1886,1889,1891,1894,1897],{"class":1318,"line":1877},38,[1316,1879,1880],{"class":1321},"      return",[1316,1882,1883],{"class":1332}," [",[1316,1885,1837],{"class":1321},[1316,1887,1888],{"class":1332},"items, { ",[1316,1890,1837],{"class":1321},[1316,1892,1893],{"class":1332},"product, quantity: ",[1316,1895,1896],{"class":1345},"1",[1316,1898,1899],{"class":1332}," }];\n",[1316,1901,1903],{"class":1318,"line":1902},39,[1316,1904,1905],{"class":1332},"    });\n",[1316,1907,1909],{"class":1318,"line":1908},40,[1316,1910,1911],{"class":1332},"  }\n",[1316,1913,1915],{"class":1318,"line":1914},41,[1316,1916,1383],{"emptyLinePlaceholder":1382},[1316,1918,1920,1923,1925,1928,1930,1932],{"class":1318,"line":1919},42,[1316,1921,1922],{"class":1478},"  remove",[1316,1924,1719],{"class":1332},[1316,1926,1927],{"class":1338},"id",[1316,1929,1342],{"class":1321},[1316,1931,1346],{"class":1345},[1316,1933,1729],{"class":1332},[1316,1935,1937,1939,1941,1943,1945,1947],{"class":1318,"line":1936},43,[1316,1938,1578],{"class":1345},[1316,1940,1545],{"class":1332},[1316,1942,1738],{"class":1478},[1316,1944,1719],{"class":1332},[1316,1946,1584],{"class":1338},[1316,1948,1815],{"class":1321},[1316,1950,1952,1955,1957,1959,1961,1963,1965,1968],{"class":1318,"line":1951},44,[1316,1953,1954],{"class":1332},"      items.",[1316,1956,1044],{"class":1478},[1316,1958,1719],{"class":1332},[1316,1960,1606],{"class":1338},[1316,1962,1745],{"class":1321},[1316,1964,1775],{"class":1332},[1316,1966,1967],{"class":1321},"!==",[1316,1969,1970],{"class":1332}," id)\n",[1316,1972,1974],{"class":1318,"line":1973},45,[1316,1975,1976],{"class":1332},"    );\n",[1316,1978,1980],{"class":1318,"line":1979},46,[1316,1981,1911],{"class":1332},[1316,1983,1985],{"class":1318,"line":1984},47,[1316,1986,1383],{"emptyLinePlaceholder":1382},[1316,1988,1990,1993],{"class":1318,"line":1989},48,[1316,1991,1992],{"class":1478},"  clear",[1316,1994,1995],{"class":1332},"() {\n",[1316,1997,1999,2001,2003,2006],{"class":1318,"line":1998},49,[1316,2000,1578],{"class":1345},[1316,2002,1545],{"class":1332},[1316,2004,2005],{"class":1478},"set",[1316,2007,2008],{"class":1332},"([]);\n",[1316,2010,2012],{"class":1318,"line":2011},50,[1316,2013,1911],{"class":1332},[1316,2015,2017],{"class":1318,"line":2016},51,[1316,2018,1377],{"class":1332},[1216,2020,2021,2022,2024,2025,2027],{},"這個 Service 已經具備基本 Store 的功能。",[1220,2023,1222],{}," 保存商品資料、",[1220,2026,1239],{}," 計算數量與總金額，Methods 則負責修改購物車內容。",[1216,2029,2030],{},"其中比較需要注意的是狀態封裝：",[1307,2032,2034],{"className":1309,"code":2033,"language":1311,"meta":1312,"style":1312},"private readonly _items = signal\u003CCartItem[]>([]);\n\nreadonly items = this._items.asReadonly();\n",[1220,2035,2036,2052,2056],{"__ignoreMap":1312},[1316,2037,2038,2041,2044,2046,2048,2050],{"class":1318,"line":340},[1316,2039,2040],{"class":1332},"private readonly _items ",[1316,2042,2043],{"class":1321},"=",[1316,2045,1517],{"class":1478},[1316,2047,1520],{"class":1332},[1316,2049,1420],{"class":1328},[1316,2051,1525],{"class":1332},[1316,2053,2054],{"class":1318,"line":329},[1316,2055,1383],{"emptyLinePlaceholder":1382},[1316,2057,2058,2061,2063,2065,2067,2069],{"class":1318,"line":318},[1316,2059,2060],{"class":1332},"readonly items ",[1316,2062,2043],{"class":1321},[1316,2064,1542],{"class":1345},[1316,2066,1545],{"class":1332},[1316,2068,1548],{"class":1478},[1316,2070,1551],{"class":1332},[1216,2072,2073,2074,2077,2078,2081,2082,2085,2086,2089,2090,2093],{},"將 ",[1220,2075,2076],{},"WritableSignal"," 設為 private，再透過 ",[1220,2079,2080],{},"asReadonly()"," 對外提供唯讀 Signal。這樣 Component 可以讀取 ",[1220,2083,2084],{},"items()","，但不能直接呼叫 ",[1220,2087,2088],{},"set()"," 或 ",[1220,2091,2092],{},"update()","，而是要透過 Store 提供的方法修改。",[1216,2095,2096,2097,2099],{},"不過，",[1220,2098,2080],{}," 不會深層凍結物件，因此仍然需要避免直接修改陣列內的商品資料。",[1216,2101,2102,2103,2105,2106,2108,2109,2112,2113,2116],{},"新增商品或調整數量時，",[1220,2104,2092],{}," 會回傳新的陣列，而不是直接修改原本的資料。當 ",[1220,2107,1584],{}," 更新後，",[1220,2110,2111],{},"count"," 和 ",[1220,2114,2115],{},"total"," 也會依據最新的商品資料計算。",[1294,2118,2120],{"id":2119},"跨-component-共用狀態","跨 Component 共用狀態",[1216,2122,2123,2124,2127],{},"有了 Store 之後，不同 Component 就能直接注入同一個 ",[1220,2125,2126],{},"CartStore","，不需要透過父元件逐層傳遞購物車資料。",[1216,2129,2130],{},[1220,2131,2132],{},"product-list.ts",[1307,2134,2136],{"className":1309,"code":2135,"language":1311,"meta":1312,"style":1312},"import { Component, inject } from '@angular\u002Fcore';\nimport { CartStore } from '.\u002Fcart.store';\n\n@Component({\n  selector: 'app-product-list',\n  template: `\n    @for (product of products; track product.id) {\n      \u003Cdiv>\n        \u003Cspan>{{ product.name }} - NT$ {{ product.price }}\u003C\u002Fspan>\n\n        \u003Cbutton (click)=\"store.add(product)\">\n          加入購物車\n        \u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    }\n  `\n})\nexport class ProductList {\n  readonly store = inject(CartStore);\n\n  readonly products = [\n    { id: 1, name: '鍵盤', price: 1200 },\n    { id: 2, name: '滑鼠', price: 800 }\n  ];\n}\n",[1220,2137,2138,2151,2165,2169,2179,2190,2198,2203,2208,2213,2217,2222,2227,2232,2237,2242,2247,2252,2263,2278,2282,2294,2316,2335,2340],{"__ignoreMap":1312},[1316,2139,2140,2142,2145,2147,2149],{"class":1318,"line":340},[1316,2141,1440],{"class":1321},[1316,2143,2144],{"class":1332}," { Component, inject } ",[1316,2146,1446],{"class":1321},[1316,2148,1450],{"class":1449},[1316,2150,1349],{"class":1332},[1316,2152,2153,2155,2158,2160,2163],{"class":1318,"line":329},[1316,2154,1440],{"class":1321},[1316,2156,2157],{"class":1332}," { CartStore } ",[1316,2159,1446],{"class":1321},[1316,2161,2162],{"class":1449}," '.\u002Fcart.store'",[1316,2164,1349],{"class":1332},[1316,2166,2167],{"class":1318,"line":318},[1316,2168,1383],{"emptyLinePlaceholder":1382},[1316,2170,2171,2173,2176],{"class":1318,"line":134},[1316,2172,1475],{"class":1332},[1316,2174,2175],{"class":1478},"Component",[1316,2177,2178],{"class":1332},"({\n",[1316,2180,2181,2184,2187],{"class":1318,"line":122},[1316,2182,2183],{"class":1332},"  selector: ",[1316,2185,2186],{"class":1449},"'app-product-list'",[1316,2188,2189],{"class":1332},",\n",[1316,2191,2192,2195],{"class":1318,"line":31},[1316,2193,2194],{"class":1332},"  template: ",[1316,2196,2197],{"class":1449},"`\n",[1316,2199,2200],{"class":1318,"line":17},[1316,2201,2202],{"class":1449},"    @for (product of products; track product.id) {\n",[1316,2204,2205],{"class":1318,"line":110},[1316,2206,2207],{"class":1449},"      \u003Cdiv>\n",[1316,2209,2210],{"class":1318,"line":76},[1316,2211,2212],{"class":1449},"        \u003Cspan>{{ product.name }} - NT$ {{ product.price }}\u003C\u002Fspan>\n",[1316,2214,2215],{"class":1318,"line":247},[1316,2216,1383],{"emptyLinePlaceholder":1382},[1316,2218,2219],{"class":1318,"line":236},[1316,2220,2221],{"class":1449},"        \u003Cbutton (click)=\"store.add(product)\">\n",[1316,2223,2224],{"class":1318,"line":225},[1316,2225,2226],{"class":1449},"          加入購物車\n",[1316,2228,2229],{"class":1318,"line":214},[1316,2230,2231],{"class":1449},"        \u003C\u002Fbutton>\n",[1316,2233,2234],{"class":1318,"line":203},[1316,2235,2236],{"class":1449},"      \u003C\u002Fdiv>\n",[1316,2238,2239],{"class":1318,"line":192},[1316,2240,2241],{"class":1449},"    }\n",[1316,2243,2244],{"class":1318,"line":181},[1316,2245,2246],{"class":1449},"  `\n",[1316,2248,2249],{"class":1318,"line":170},[1316,2250,2251],{"class":1332},"})\n",[1316,2253,2254,2256,2258,2261],{"class":1318,"line":159},[1316,2255,1322],{"class":1321},[1316,2257,1495],{"class":1321},[1316,2259,2260],{"class":1328}," ProductList",[1316,2262,1333],{"class":1332},[1316,2264,2265,2267,2270,2272,2275],{"class":1318,"line":148},[1316,2266,1534],{"class":1321},[1316,2268,2269],{"class":1338}," store",[1316,2271,1514],{"class":1321},[1316,2273,2274],{"class":1478}," inject",[1316,2276,2277],{"class":1332},"(CartStore);\n",[1316,2279,2280],{"class":1318,"line":137},[1316,2281,1383],{"emptyLinePlaceholder":1382},[1316,2283,2284,2286,2289,2291],{"class":1318,"line":125},[1316,2285,1534],{"class":1321},[1316,2287,2288],{"class":1338}," products",[1316,2290,1514],{"class":1321},[1316,2292,2293],{"class":1332}," [\n",[1316,2295,2296,2299,2301,2304,2307,2310,2313],{"class":1318,"line":113},[1316,2297,2298],{"class":1332},"    { id: ",[1316,2300,1896],{"class":1345},[1316,2302,2303],{"class":1332},", name: ",[1316,2305,2306],{"class":1449},"'鍵盤'",[1316,2308,2309],{"class":1332},", price: ",[1316,2311,2312],{"class":1345},"1200",[1316,2314,2315],{"class":1332}," },\n",[1316,2317,2318,2320,2323,2325,2328,2330,2333],{"class":1318,"line":101},[1316,2319,2298],{"class":1332},[1316,2321,2322],{"class":1345},"2",[1316,2324,2303],{"class":1332},[1316,2326,2327],{"class":1449},"'滑鼠'",[1316,2329,2309],{"class":1332},[1316,2331,2332],{"class":1345},"800",[1316,2334,1848],{"class":1332},[1316,2336,2337],{"class":1318,"line":90},[1316,2338,2339],{"class":1332},"  ];\n",[1316,2341,2342],{"class":1318,"line":79},[1316,2343,1377],{"class":1332},[1216,2345,2346],{},"Navbar 只需要讀取購物車數量：",[1216,2348,2349],{},[1220,2350,2351],{},"navbar.ts",[1307,2353,2355],{"className":1309,"code":2354,"language":1311,"meta":1312,"style":1312},"import { Component, inject } from '@angular\u002Fcore';\nimport { CartStore } from '.\u002Fcart.store';\n\n@Component({\n  selector: 'app-navbar',\n  template: `\n    \u003Cnav>\n      購物車 ({{ store.count() }})\n    \u003C\u002Fnav>\n  `\n})\nexport class Navbar {\n  readonly store = inject(CartStore);\n}\n",[1220,2356,2357,2369,2381,2385,2393,2402,2408,2413,2418,2423,2427,2431,2442,2454],{"__ignoreMap":1312},[1316,2358,2359,2361,2363,2365,2367],{"class":1318,"line":340},[1316,2360,1440],{"class":1321},[1316,2362,2144],{"class":1332},[1316,2364,1446],{"class":1321},[1316,2366,1450],{"class":1449},[1316,2368,1349],{"class":1332},[1316,2370,2371,2373,2375,2377,2379],{"class":1318,"line":329},[1316,2372,1440],{"class":1321},[1316,2374,2157],{"class":1332},[1316,2376,1446],{"class":1321},[1316,2378,2162],{"class":1449},[1316,2380,1349],{"class":1332},[1316,2382,2383],{"class":1318,"line":318},[1316,2384,1383],{"emptyLinePlaceholder":1382},[1316,2386,2387,2389,2391],{"class":1318,"line":134},[1316,2388,1475],{"class":1332},[1316,2390,2175],{"class":1478},[1316,2392,2178],{"class":1332},[1316,2394,2395,2397,2400],{"class":1318,"line":122},[1316,2396,2183],{"class":1332},[1316,2398,2399],{"class":1449},"'app-navbar'",[1316,2401,2189],{"class":1332},[1316,2403,2404,2406],{"class":1318,"line":31},[1316,2405,2194],{"class":1332},[1316,2407,2197],{"class":1449},[1316,2409,2410],{"class":1318,"line":17},[1316,2411,2412],{"class":1449},"    \u003Cnav>\n",[1316,2414,2415],{"class":1318,"line":110},[1316,2416,2417],{"class":1449},"      購物車 ({{ store.count() }})\n",[1316,2419,2420],{"class":1318,"line":76},[1316,2421,2422],{"class":1449},"    \u003C\u002Fnav>\n",[1316,2424,2425],{"class":1318,"line":247},[1316,2426,2246],{"class":1449},[1316,2428,2429],{"class":1318,"line":236},[1316,2430,2251],{"class":1332},[1316,2432,2433,2435,2437,2440],{"class":1318,"line":225},[1316,2434,1322],{"class":1321},[1316,2436,1495],{"class":1321},[1316,2438,2439],{"class":1328}," Navbar",[1316,2441,1333],{"class":1332},[1316,2443,2444,2446,2448,2450,2452],{"class":1318,"line":214},[1316,2445,1534],{"class":1321},[1316,2447,2269],{"class":1338},[1316,2449,1514],{"class":1321},[1316,2451,2274],{"class":1478},[1316,2453,2277],{"class":1332},[1316,2455,2456],{"class":1318,"line":203},[1316,2457,1377],{"class":1332},[1216,2459,2460],{},"購物車頁面則顯示商品清單、總金額，並提供刪除與清空功能：",[1216,2462,2463],{},[1220,2464,2465],{},"cart.ts",[1307,2467,2469],{"className":1309,"code":2468,"language":1311,"meta":1312,"style":1312},"import { Component, inject } from '@angular\u002Fcore';\nimport { CartStore } from '.\u002Fcart.store';\n\n@Component({\n  selector: 'app-cart',\n  template: `\n    @for (item of store.items(); track item.id) {\n      \u003Cdiv>\n        {{ item.name }} × {{ item.quantity }}\n\n        \u003Cbutton (click)=\"store.remove(item.id)\">\n          刪除\n        \u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    }\n\n    \u003Cp>總金額：NT$ {{ store.total() }}\u003C\u002Fp>\n\n    \u003Cbutton (click)=\"store.clear()\">\n      清空購物車\n    \u003C\u002Fbutton>\n  `\n})\nexport class Cart {\n  readonly store = inject(CartStore);\n}\n",[1220,2470,2471,2483,2495,2499,2507,2516,2522,2527,2531,2536,2540,2545,2550,2554,2558,2562,2566,2571,2575,2580,2585,2590,2594,2598,2609,2621],{"__ignoreMap":1312},[1316,2472,2473,2475,2477,2479,2481],{"class":1318,"line":340},[1316,2474,1440],{"class":1321},[1316,2476,2144],{"class":1332},[1316,2478,1446],{"class":1321},[1316,2480,1450],{"class":1449},[1316,2482,1349],{"class":1332},[1316,2484,2485,2487,2489,2491,2493],{"class":1318,"line":329},[1316,2486,1440],{"class":1321},[1316,2488,2157],{"class":1332},[1316,2490,1446],{"class":1321},[1316,2492,2162],{"class":1449},[1316,2494,1349],{"class":1332},[1316,2496,2497],{"class":1318,"line":318},[1316,2498,1383],{"emptyLinePlaceholder":1382},[1316,2500,2501,2503,2505],{"class":1318,"line":134},[1316,2502,1475],{"class":1332},[1316,2504,2175],{"class":1478},[1316,2506,2178],{"class":1332},[1316,2508,2509,2511,2514],{"class":1318,"line":122},[1316,2510,2183],{"class":1332},[1316,2512,2513],{"class":1449},"'app-cart'",[1316,2515,2189],{"class":1332},[1316,2517,2518,2520],{"class":1318,"line":31},[1316,2519,2194],{"class":1332},[1316,2521,2197],{"class":1449},[1316,2523,2524],{"class":1318,"line":17},[1316,2525,2526],{"class":1449},"    @for (item of store.items(); track item.id) {\n",[1316,2528,2529],{"class":1318,"line":110},[1316,2530,2207],{"class":1449},[1316,2532,2533],{"class":1318,"line":76},[1316,2534,2535],{"class":1449},"        {{ item.name }} × {{ item.quantity }}\n",[1316,2537,2538],{"class":1318,"line":247},[1316,2539,1383],{"emptyLinePlaceholder":1382},[1316,2541,2542],{"class":1318,"line":236},[1316,2543,2544],{"class":1449},"        \u003Cbutton (click)=\"store.remove(item.id)\">\n",[1316,2546,2547],{"class":1318,"line":225},[1316,2548,2549],{"class":1449},"          刪除\n",[1316,2551,2552],{"class":1318,"line":214},[1316,2553,2231],{"class":1449},[1316,2555,2556],{"class":1318,"line":203},[1316,2557,2236],{"class":1449},[1316,2559,2560],{"class":1318,"line":192},[1316,2561,2241],{"class":1449},[1316,2563,2564],{"class":1318,"line":181},[1316,2565,1383],{"emptyLinePlaceholder":1382},[1316,2567,2568],{"class":1318,"line":170},[1316,2569,2570],{"class":1449},"    \u003Cp>總金額：NT$ {{ store.total() }}\u003C\u002Fp>\n",[1316,2572,2573],{"class":1318,"line":159},[1316,2574,1383],{"emptyLinePlaceholder":1382},[1316,2576,2577],{"class":1318,"line":148},[1316,2578,2579],{"class":1449},"    \u003Cbutton (click)=\"store.clear()\">\n",[1316,2581,2582],{"class":1318,"line":137},[1316,2583,2584],{"class":1449},"      清空購物車\n",[1316,2586,2587],{"class":1318,"line":125},[1316,2588,2589],{"class":1449},"    \u003C\u002Fbutton>\n",[1316,2591,2592],{"class":1318,"line":113},[1316,2593,2246],{"class":1449},[1316,2595,2596],{"class":1318,"line":101},[1316,2597,2251],{"class":1332},[1316,2599,2600,2602,2604,2607],{"class":1318,"line":90},[1316,2601,1322],{"class":1321},[1316,2603,1495],{"class":1321},[1316,2605,2606],{"class":1328}," Cart",[1316,2608,1333],{"class":1332},[1316,2610,2611,2613,2615,2617,2619],{"class":1318,"line":79},[1316,2612,1534],{"class":1321},[1316,2614,2269],{"class":1338},[1316,2616,1514],{"class":1321},[1316,2618,2274],{"class":1478},[1316,2620,2277],{"class":1332},[1316,2622,2623],{"class":1318,"line":67},[1316,2624,1377],{"class":1332},[1216,2626,2627,2628,2630,2631,2634],{},"由於 ",[1220,2629,2126],{}," 設定了 ",[1220,2632,2633],{},"providedIn: 'root'","，這三個 Component 在沒有覆寫 Provider 的情況下，會取得同一個 Service 實體。",[1216,2636,2637],{},"所以當商品列表加入商品時，Navbar 的數量與購物車頁面的總金額也會跟著更新。",[1216,2639,2640],{},"到這裡，我們已經使用原生 Signal 完成一個簡單的購物車 Store，完全不需要 NgRx。",[1245,2642,2644],{"id":2643},"改用-ngrx-signalstore","改用 NgRx SignalStore",[1216,2646,2647],{},"既然原生 Signal 就能建立 Store，為什麼還需要 NgRx SignalStore？",[1216,2649,2650],{},"當 Store 開始加入更多狀態、非同步請求與操作方法時，Service 可能會越寫越大。雖然可以自行拆分，但也需要考慮各個 Store 的程式碼該怎麼組織，以及共通功能要怎麼重複使用。",[1216,2652,2653],{},"NgRx SignalStore 是 NgRx 提供的狀態管理工具，建立在 Angular 原生 Signal 之上。它將 State、Computed 與 Methods 拆成不同的 Feature，讓我們可以依照需求組合 Store。",[1216,2655,2656],{},"這裡要注意，NgRx SignalStore 與傳統 NgRx Store 是不同的方案。傳統 NgRx Store 主要使用 Action、Reducer 等架構，而 SignalStore 則以 Signal 與 Method 為核心。",[1216,2658,2659],{},"這次會使用 NgRx SignalStore 22，搭配 Angular 22 進行實作。",[1294,2661,2663],{"id":2662},"建立-ngrx-signalstore","建立 NgRx SignalStore",[1216,2665,2666],{},"NgRx SignalStore 主要提供以下 API：",[2668,2669,2670,2683],"table",{},[2671,2672,2673],"thead",{},[2674,2675,2676,2680],"tr",{},[2677,2678,2679],"th",{},"API",[2677,2681,2682],{},"用途",[2684,2685,2686,2697,2707,2717,2727],"tbody",{},[2674,2687,2688,2694],{},[2689,2690,2691],"td",{},[1220,2692,2693],{},"signalStore()",[2689,2695,2696],{},"建立 Store",[2674,2698,2699,2704],{},[2689,2700,2701],{},[1220,2702,2703],{},"withState()",[2689,2705,2706],{},"定義初始狀態",[2674,2708,2709,2714],{},[2689,2710,2711],{},[1220,2712,2713],{},"withComputed()",[2689,2715,2716],{},"建立衍生狀態",[2674,2718,2719,2724],{},[2689,2720,2721],{},[1220,2722,2723],{},"withMethods()",[2689,2725,2726],{},"定義操作方法",[2674,2728,2729,2734],{},[2689,2730,2731],{},[1220,2732,2733],{},"patchState()",[2689,2735,2736],{},"更新 Store 狀態",[1216,2738,2739],{},"我們使用相同的購物車需求重新實作。",[1216,2741,2742],{},[1220,2743,2744],{},"ngrx-cart.store.ts",[1307,2746,2748],{"className":1309,"code":2747,"language":1311,"meta":1312,"style":1312},"import { computed } from '@angular\u002Fcore';\nimport {\n  signalStore,\n  withState,\n  withComputed,\n  withMethods,\n  patchState\n} from '@ngrx\u002Fsignals';\n\nimport { CartItem, Product } from '.\u002Fcart.types';\n\nexport const NgRxCartStore = signalStore(\n  { providedIn: 'root' },\n\n  withState({\n    items: [] as CartItem[]\n  }),\n\n  withComputed(({ items }) => ({\n    count: computed(() =>\n      items().reduce(\n        (sum, item) => sum + item.quantity,\n        0\n      )\n    ),\n\n    total: computed(() =>\n      items().reduce(\n        (sum, item) => sum + item.price * item.quantity,\n        0\n      )\n    )\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",[1220,2749,2750,2763,2769,2774,2779,2784,2789,2794,2806,2810,2822,2826,2843,2852,2856,2863,2876,2881,2885,2903,2915,2926,2947,2952,2957,2962,2966,2977,2987,3011,3015,3019,3023,3028,3032,3046,3061,3076,3092,3105,3109,3113,3124,3139,3148,3165,3172,3177,3197,3201,3208,3213,3219,3224,3240,3253,3273,3279,3284,3289,3297,3305,3310,3316],{"__ignoreMap":1312},[1316,2751,2752,2754,2757,2759,2761],{"class":1318,"line":340},[1316,2753,1440],{"class":1321},[1316,2755,2756],{"class":1332}," { computed } ",[1316,2758,1446],{"class":1321},[1316,2760,1450],{"class":1449},[1316,2762,1349],{"class":1332},[1316,2764,2765,2767],{"class":1318,"line":329},[1316,2766,1440],{"class":1321},[1316,2768,1333],{"class":1332},[1316,2770,2771],{"class":1318,"line":318},[1316,2772,2773],{"class":1332},"  signalStore,\n",[1316,2775,2776],{"class":1318,"line":134},[1316,2777,2778],{"class":1332},"  withState,\n",[1316,2780,2781],{"class":1318,"line":122},[1316,2782,2783],{"class":1332},"  withComputed,\n",[1316,2785,2786],{"class":1318,"line":31},[1316,2787,2788],{"class":1332},"  withMethods,\n",[1316,2790,2791],{"class":1318,"line":17},[1316,2792,2793],{"class":1332},"  patchState\n",[1316,2795,2796,2799,2801,2804],{"class":1318,"line":110},[1316,2797,2798],{"class":1332},"} ",[1316,2800,1446],{"class":1321},[1316,2802,2803],{"class":1449}," '@ngrx\u002Fsignals'",[1316,2805,1349],{"class":1332},[1316,2807,2808],{"class":1318,"line":76},[1316,2809,1383],{"emptyLinePlaceholder":1382},[1316,2811,2812,2814,2816,2818,2820],{"class":1318,"line":247},[1316,2813,1440],{"class":1321},[1316,2815,1459],{"class":1332},[1316,2817,1446],{"class":1321},[1316,2819,1464],{"class":1449},[1316,2821,1349],{"class":1332},[1316,2823,2824],{"class":1318,"line":236},[1316,2825,1383],{"emptyLinePlaceholder":1382},[1316,2827,2828,2830,2833,2836,2838,2841],{"class":1318,"line":225},[1316,2829,1322],{"class":1321},[1316,2831,2832],{"class":1321}," const",[1316,2834,2835],{"class":1345}," NgRxCartStore",[1316,2837,1514],{"class":1321},[1316,2839,2840],{"class":1478}," signalStore",[1316,2842,1592],{"class":1332},[1316,2844,2845,2848,2850],{"class":1318,"line":214},[1316,2846,2847],{"class":1332},"  { providedIn: ",[1316,2849,1485],{"class":1449},[1316,2851,2315],{"class":1332},[1316,2853,2854],{"class":1318,"line":203},[1316,2855,1383],{"emptyLinePlaceholder":1382},[1316,2857,2858,2861],{"class":1318,"line":192},[1316,2859,2860],{"class":1478},"  withState",[1316,2862,2178],{"class":1332},[1316,2864,2865,2868,2871,2873],{"class":1318,"line":181},[1316,2866,2867],{"class":1332},"    items: [] ",[1316,2869,2870],{"class":1321},"as",[1316,2872,1392],{"class":1328},[1316,2874,2875],{"class":1332},"[]\n",[1316,2877,2878],{"class":1318,"line":170},[1316,2879,2880],{"class":1332},"  }),\n",[1316,2882,2883],{"class":1318,"line":159},[1316,2884,1383],{"emptyLinePlaceholder":1382},[1316,2886,2887,2890,2893,2895,2898,2900],{"class":1318,"line":148},[1316,2888,2889],{"class":1478},"  withComputed",[1316,2891,2892],{"class":1332},"(({ ",[1316,2894,1584],{"class":1338},[1316,2896,2897],{"class":1332}," }) ",[1316,2899,1612],{"class":1321},[1316,2901,2902],{"class":1332}," ({\n",[1316,2904,2905,2908,2911,2913],{"class":1318,"line":137},[1316,2906,2907],{"class":1332},"    count: ",[1316,2909,2910],{"class":1478},"computed",[1316,2912,1570],{"class":1332},[1316,2914,1573],{"class":1321},[1316,2916,2917,2920,2922,2924],{"class":1318,"line":125},[1316,2918,2919],{"class":1478},"      items",[1316,2921,1587],{"class":1332},[1316,2923,1045],{"class":1478},[1316,2925,1592],{"class":1332},[1316,2927,2928,2931,2933,2935,2937,2939,2941,2943,2945],{"class":1318,"line":113},[1316,2929,2930],{"class":1332},"        (",[1316,2932,1600],{"class":1338},[1316,2934,1603],{"class":1332},[1316,2936,1606],{"class":1338},[1316,2938,1609],{"class":1332},[1316,2940,1612],{"class":1321},[1316,2942,1615],{"class":1332},[1316,2944,1618],{"class":1321},[1316,2946,1621],{"class":1332},[1316,2948,2949],{"class":1318,"line":101},[1316,2950,2951],{"class":1345},"        0\n",[1316,2953,2954],{"class":1318,"line":90},[1316,2955,2956],{"class":1332},"      )\n",[1316,2958,2959],{"class":1318,"line":79},[1316,2960,2961],{"class":1332},"    ),\n",[1316,2963,2964],{"class":1318,"line":67},[1316,2965,1383],{"emptyLinePlaceholder":1382},[1316,2967,2968,2971,2973,2975],{"class":1318,"line":56},[1316,2969,2970],{"class":1332},"    total: ",[1316,2972,2910],{"class":1478},[1316,2974,1570],{"class":1332},[1316,2976,1573],{"class":1321},[1316,2978,2979,2981,2983,2985],{"class":1318,"line":45},[1316,2980,2919],{"class":1478},[1316,2982,1587],{"class":1332},[1316,2984,1045],{"class":1478},[1316,2986,1592],{"class":1332},[1316,2988,2989,2991,2993,2995,2997,2999,3001,3003,3005,3007,3009],{"class":1318,"line":34},[1316,2990,2930],{"class":1332},[1316,2992,1600],{"class":1338},[1316,2994,1603],{"class":1332},[1316,2996,1606],{"class":1338},[1316,2998,1609],{"class":1332},[1316,3000,1612],{"class":1321},[1316,3002,1615],{"class":1332},[1316,3004,1618],{"class":1321},[1316,3006,1690],{"class":1332},[1316,3008,1693],{"class":1321},[1316,3010,1621],{"class":1332},[1316,3012,3013],{"class":1318,"line":22},[1316,3014,2951],{"class":1345},[1316,3016,3017],{"class":1318,"line":1801},[1316,3018,2956],{"class":1332},[1316,3020,3021],{"class":1318,"line":1818},[1316,3022,1631],{"class":1332},[1316,3024,3025],{"class":1318,"line":1828},[1316,3026,3027],{"class":1332},"  })),\n",[1316,3029,3030],{"class":1318,"line":1851},[1316,3031,1383],{"emptyLinePlaceholder":1382},[1316,3033,3034,3037,3039,3042,3044],{"class":1318,"line":1860},[1316,3035,3036],{"class":1478},"  withMethods",[1316,3038,1719],{"class":1332},[1316,3040,3041],{"class":1338},"store",[1316,3043,1745],{"class":1321},[1316,3045,2902],{"class":1332},[1316,3047,3048,3051,3053,3055,3057,3059],{"class":1318,"line":1866},[1316,3049,3050],{"class":1478},"    add",[1316,3052,1719],{"class":1332},[1316,3054,1722],{"class":1338},[1316,3056,1342],{"class":1321},[1316,3058,1329],{"class":1328},[1316,3060,1729],{"class":1332},[1316,3062,3063,3066,3069,3072,3074],{"class":1318,"line":1872},[1316,3064,3065],{"class":1478},"      patchState",[1316,3067,3068],{"class":1332},"(store, ",[1316,3070,3071],{"class":1338},"state",[1316,3073,1745],{"class":1321},[1316,3075,1333],{"class":1332},[1316,3077,3078,3081,3083,3085,3088,3090],{"class":1318,"line":1877},[1316,3079,3080],{"class":1321},"        const",[1316,3082,1755],{"class":1345},[1316,3084,1514],{"class":1321},[1316,3086,3087],{"class":1332}," state.items.",[1316,3089,1763],{"class":1478},[1316,3091,1592],{"class":1332},[1316,3093,3094,3097,3099,3101,3103],{"class":1318,"line":1902},[1316,3095,3096],{"class":1338},"          item",[1316,3098,1745],{"class":1321},[1316,3100,1775],{"class":1332},[1316,3102,1778],{"class":1321},[1316,3104,1781],{"class":1332},[1316,3106,3107],{"class":1318,"line":1908},[1316,3108,1863],{"class":1332},[1316,3110,3111],{"class":1318,"line":1914},[1316,3112,1383],{"emptyLinePlaceholder":1382},[1316,3114,3115,3117,3119,3121],{"class":1318,"line":1919},[1316,3116,3080],{"class":1321},[1316,3118,1537],{"class":1345},[1316,3120,1514],{"class":1321},[1316,3122,3123],{"class":1332}," existing\n",[1316,3125,3126,3129,3131,3133,3135,3137],{"class":1318,"line":1936},[1316,3127,3128],{"class":1321},"          ?",[1316,3130,3087],{"class":1332},[1316,3132,1043],{"class":1478},[1316,3134,1719],{"class":1332},[1316,3136,1606],{"class":1338},[1316,3138,1815],{"class":1321},[1316,3140,3141,3144,3146],{"class":1318,"line":1951},[1316,3142,3143],{"class":1332},"              item.id ",[1316,3145,1778],{"class":1321},[1316,3147,1781],{"class":1332},[1316,3149,3150,3153,3155,3157,3159,3161,3163],{"class":1318,"line":1973},[1316,3151,3152],{"class":1321},"                ?",[1316,3154,1834],{"class":1332},[1316,3156,1837],{"class":1321},[1316,3158,1840],{"class":1332},[1316,3160,1618],{"class":1321},[1316,3162,1845],{"class":1345},[1316,3164,1848],{"class":1332},[1316,3166,3167,3170],{"class":1318,"line":1979},[1316,3168,3169],{"class":1321},"                :",[1316,3171,1857],{"class":1332},[1316,3173,3174],{"class":1318,"line":1984},[1316,3175,3176],{"class":1332},"            )\n",[1316,3178,3179,3182,3184,3186,3189,3191,3193,3195],{"class":1318,"line":1989},[1316,3180,3181],{"class":1321},"          :",[1316,3183,1883],{"class":1332},[1316,3185,1837],{"class":1321},[1316,3187,3188],{"class":1332},"state.items, { ",[1316,3190,1837],{"class":1321},[1316,3192,1893],{"class":1332},[1316,3194,1896],{"class":1345},[1316,3196,1899],{"class":1332},[1316,3198,3199],{"class":1318,"line":1998},[1316,3200,1383],{"emptyLinePlaceholder":1382},[1316,3202,3203,3205],{"class":1318,"line":2011},[1316,3204,1804],{"class":1321},[1316,3206,3207],{"class":1332}," { items };\n",[1316,3209,3210],{"class":1318,"line":2016},[1316,3211,3212],{"class":1332},"      });\n",[1316,3214,3216],{"class":1318,"line":3215},52,[1316,3217,3218],{"class":1332},"    },\n",[1316,3220,3222],{"class":1318,"line":3221},53,[1316,3223,1383],{"emptyLinePlaceholder":1382},[1316,3225,3227,3230,3232,3234,3236,3238],{"class":1318,"line":3226},54,[1316,3228,3229],{"class":1478},"    remove",[1316,3231,1719],{"class":1332},[1316,3233,1927],{"class":1338},[1316,3235,1342],{"class":1321},[1316,3237,1346],{"class":1345},[1316,3239,1729],{"class":1332},[1316,3241,3243,3245,3247,3249,3251],{"class":1318,"line":3242},55,[1316,3244,3065],{"class":1478},[1316,3246,3068],{"class":1332},[1316,3248,3071],{"class":1338},[1316,3250,1745],{"class":1321},[1316,3252,2902],{"class":1332},[1316,3254,3256,3259,3261,3263,3265,3267,3269,3271],{"class":1318,"line":3255},56,[1316,3257,3258],{"class":1332},"        items: state.items.",[1316,3260,1044],{"class":1478},[1316,3262,1719],{"class":1332},[1316,3264,1606],{"class":1338},[1316,3266,1745],{"class":1321},[1316,3268,1775],{"class":1332},[1316,3270,1967],{"class":1321},[1316,3272,1970],{"class":1332},[1316,3274,3276],{"class":1318,"line":3275},57,[1316,3277,3278],{"class":1332},"      }));\n",[1316,3280,3282],{"class":1318,"line":3281},58,[1316,3283,3218],{"class":1332},[1316,3285,3287],{"class":1318,"line":3286},59,[1316,3288,1383],{"emptyLinePlaceholder":1382},[1316,3290,3292,3295],{"class":1318,"line":3291},60,[1316,3293,3294],{"class":1478},"    clear",[1316,3296,1995],{"class":1332},[1316,3298,3300,3302],{"class":1318,"line":3299},61,[1316,3301,3065],{"class":1478},[1316,3303,3304],{"class":1332},"(store, { items: [] });\n",[1316,3306,3308],{"class":1318,"line":3307},62,[1316,3309,2241],{"class":1332},[1316,3311,3313],{"class":1318,"line":3312},63,[1316,3314,3315],{"class":1332},"  }))\n",[1316,3317,3319],{"class":1318,"line":3318},64,[1316,3320,3321],{"class":1332},");\n",[1216,3323,3324],{},"購物車的功能沒有改變，但 State、Computed 與 Methods 有了各自的位置。",[1216,3326,3327,3328,3330,3331,3333,3334,3336],{},"原本需要自行建立 private ",[1220,3329,1222],{},"，再透過 ",[1220,3332,2080],{}," 對外提供資料；SignalStore 則使用 ",[1220,3335,2703],{}," 定義狀態，並自動提供對應的唯讀 Signal。",[1216,3338,3339,3340,2089,3342,3344,3345,3347],{},"修改狀態時，原本使用的 ",[1220,3341,2088],{},[1220,3343,2092],{},"，現在則改成 NgRx 提供的 ",[1220,3346,2733],{},"：",[1307,3349,3351],{"className":1309,"code":3350,"language":1311,"meta":1312,"style":1312},"patchState(store, { items: [] });\n",[1220,3352,3353],{"__ignoreMap":1312},[1316,3354,3355,3358],{"class":1318,"line":340},[1316,3356,3357],{"class":1478},"patchState",[1316,3359,3304],{"class":1332},[1216,3361,3362,3364],{},[1220,3363,2733],{}," 可以直接指定新值，也能根據目前的 State 計算更新結果，而且只會修改指定的屬性。",[1216,3366,3367,3368,3370,3371,1236,3374,3377,3378,3380],{},"它通常會搭配 ",[1220,3369,2723],{}," 使用，讓 Component 透過 ",[1220,3372,3373],{},"add()",[1220,3375,3376],{},"remove()"," 等方法操作狀態。SignalStore 預設也會保護 State，外部無法直接對受保護的 Store 呼叫 ",[1220,3379,2733],{},"，避免狀態被隨意修改。",[1216,3382,3383],{},[1289,3384],{"alt":3385,"src":3386},"原生 Signal + Service 與 NgRx SignalStore 的對應關係","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791552133\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2025%EF%BC%9AAngular%20SignalStore%EF%BC%8C%E5%8E%9F%E7%94%9F%20Signal%20%E4%B8%8D%E5%A4%A0%E7%94%A8%E5%97%8E%EF%BC%9F\u002Fnative-signal-vs-signalstore.png",[1294,3388,3390],{"id":3389},"component-需要調整嗎","Component 需要調整嗎？",[1216,3392,3393],{},"如果讓前面的 Navbar 改用 NgRx SignalStore，只需要注入新的 Store：",[1307,3395,3397],{"className":1309,"code":3396,"language":1311,"meta":1312,"style":1312},"import { Component, inject } from '@angular\u002Fcore';\nimport { NgRxCartStore } from '.\u002Fngrx-cart.store';\n\n@Component({\n  selector: 'app-navbar',\n  template: `\n    \u003Cnav>\n      購物車 ({{ store.count() }})\n    \u003C\u002Fnav>\n  `\n})\nexport class Navbar {\n  readonly store = inject(NgRxCartStore);\n}\n",[1220,3398,3399,3411,3425,3429,3437,3445,3451,3455,3459,3463,3467,3471,3481,3494],{"__ignoreMap":1312},[1316,3400,3401,3403,3405,3407,3409],{"class":1318,"line":340},[1316,3402,1440],{"class":1321},[1316,3404,2144],{"class":1332},[1316,3406,1446],{"class":1321},[1316,3408,1450],{"class":1449},[1316,3410,1349],{"class":1332},[1316,3412,3413,3415,3418,3420,3423],{"class":1318,"line":329},[1316,3414,1440],{"class":1321},[1316,3416,3417],{"class":1332}," { NgRxCartStore } ",[1316,3419,1446],{"class":1321},[1316,3421,3422],{"class":1449}," '.\u002Fngrx-cart.store'",[1316,3424,1349],{"class":1332},[1316,3426,3427],{"class":1318,"line":318},[1316,3428,1383],{"emptyLinePlaceholder":1382},[1316,3430,3431,3433,3435],{"class":1318,"line":134},[1316,3432,1475],{"class":1332},[1316,3434,2175],{"class":1478},[1316,3436,2178],{"class":1332},[1316,3438,3439,3441,3443],{"class":1318,"line":122},[1316,3440,2183],{"class":1332},[1316,3442,2399],{"class":1449},[1316,3444,2189],{"class":1332},[1316,3446,3447,3449],{"class":1318,"line":31},[1316,3448,2194],{"class":1332},[1316,3450,2197],{"class":1449},[1316,3452,3453],{"class":1318,"line":17},[1316,3454,2412],{"class":1449},[1316,3456,3457],{"class":1318,"line":110},[1316,3458,2417],{"class":1449},[1316,3460,3461],{"class":1318,"line":76},[1316,3462,2422],{"class":1449},[1316,3464,3465],{"class":1318,"line":247},[1316,3466,2246],{"class":1449},[1316,3468,3469],{"class":1318,"line":236},[1316,3470,2251],{"class":1332},[1316,3472,3473,3475,3477,3479],{"class":1318,"line":225},[1316,3474,1322],{"class":1321},[1316,3476,1495],{"class":1321},[1316,3478,2439],{"class":1328},[1316,3480,1333],{"class":1332},[1316,3482,3483,3485,3487,3489,3491],{"class":1318,"line":214},[1316,3484,1534],{"class":1321},[1316,3486,2269],{"class":1338},[1316,3488,1514],{"class":1321},[1316,3490,2274],{"class":1478},[1316,3492,3493],{"class":1332},"(NgRxCartStore);\n",[1316,3495,3496],{"class":1318,"line":203},[1316,3497,1377],{"class":1332},[1216,3499,3500,3501,3504],{},"Template 仍然透過 ",[1220,3502,3503],{},"store.count()"," 讀取資料，因為 SignalStore 對外提供的也是 Signal。",[1216,3506,3507],{},"這次兩個 Store 剛好使用相同的 Property 與 Method 名稱，因此 Component 的寫法很接近。不過，如果要在實際專案中替換 Store，仍然需要確認原本的功能與介面是否一致。",[1245,3509,3511],{"id":3510},"原生-signal-與-ngrx-signalstore-差在哪裡","原生 Signal 與 NgRx SignalStore 差在哪裡？",[1216,3513,3514],{},"從剛剛的購物車實作可以發現，兩種方式都能完成相同的功能，而且使用 SignalStore 不一定能減少程式碼。",[1216,3516,3517],{},"SignalStore 比較大的差別，是它提供了一套可以組合與重複使用的 Store 結構。",[2668,3519,3520,3533],{},[2671,3521,3522],{},[2674,3523,3524,3527,3530],{},[2677,3525,3526],{},"比較項目",[2677,3528,3529],{},"原生 Signal + Service",[2677,3531,3532],{},"NgRx SignalStore",[2684,3534,3535,3548,3561,3576,3588,3601,3616,3629],{},[2674,3536,3537,3540,3544],{},[2689,3538,3539],{},"狀態管理",[2689,3541,3542],{},[1220,3543,1222],{},[2689,3545,3546],{},[1220,3547,2703],{},[2674,3549,3550,3553,3557],{},[2689,3551,3552],{},"衍生狀態",[2689,3554,3555],{},[1220,3556,1239],{},[2689,3558,3559],{},[1220,3560,2713],{},[2674,3562,3563,3566,3572],{},[2689,3564,3565],{},"修改狀態",[2689,3567,3568,1236,3570],{},[1220,3569,2088],{},[1220,3571,2092],{},[2689,3573,3574],{},[1220,3575,2733],{},[2674,3577,3578,3581,3584],{},[2689,3579,3580],{},"操作方法",[2689,3582,3583],{},"Service Methods",[2689,3585,3586],{},[1220,3587,2723],{},[2674,3589,3590,3593,3598],{},[2689,3591,3592],{},"狀態封裝",[2689,3594,3595,3596],{},"private、",[1220,3597,2080],{},[2689,3599,3600],{},"預設保護 State",[2674,3602,3603,3606,3609],{},[2689,3604,3605],{},"非同步操作",[2689,3607,3608],{},"Promise、RxJS、Resource 等",[2689,3610,3611,3612,3615],{},"Promise、",[1220,3613,3614],{},"rxMethod()"," 等",[2674,3617,3618,3621,3624],{},[2689,3619,3620],{},"共通邏輯擴充",[2689,3622,3623],{},"Service、函式或組合設計",[2689,3625,3626],{},[1220,3627,3628],{},"signalStoreFeature()",[2674,3630,3631,3634,3637],{},[2689,3632,3633],{},"額外依賴",[2689,3635,3636],{},"無",[2689,3638,3639],{},[1220,3640,3641],{},"@ngrx\u002Fsignals",[1216,3643,3644],{},"以目前的購物車來說，原生 Signal 就很夠用了。但如果之後需要加入 API 請求、Loading、Error，或是多個 Store 都需要相同的功能，就可以考慮 SignalStore 的 Feature 設計。",[1294,3646,3648],{"id":3647},"ngrx-signalstore-的擴充能力","NgRx SignalStore 的擴充能力",[1216,3650,3651],{},"例如購物車需要從後端載入資料，可以在 Store 中加入 Loading 與 Error 狀態：",[1307,3653,3655],{"className":1309,"code":3654,"language":1311,"meta":1312,"style":1312},"withState({\n  items: [] as CartItem[],\n  isLoading: false,\n  error: null as string | null\n})\n",[1220,3656,3657,3664,3676,3686,3705],{"__ignoreMap":1312},[1316,3658,3659,3662],{"class":1318,"line":340},[1316,3660,3661],{"class":1478},"withState",[1316,3663,2178],{"class":1332},[1316,3665,3666,3669,3671,3673],{"class":1318,"line":329},[1316,3667,3668],{"class":1332},"  items: [] ",[1316,3670,2870],{"class":1321},[1316,3672,1392],{"class":1328},[1316,3674,3675],{"class":1332},"[],\n",[1316,3677,3678,3681,3684],{"class":1318,"line":318},[1316,3679,3680],{"class":1332},"  isLoading: ",[1316,3682,3683],{"class":1345},"false",[1316,3685,2189],{"class":1332},[1316,3687,3688,3691,3694,3697,3699,3702],{"class":1318,"line":134},[1316,3689,3690],{"class":1332},"  error: ",[1316,3692,3693],{"class":1345},"null",[1316,3695,3696],{"class":1321}," as",[1316,3698,1359],{"class":1345},[1316,3700,3701],{"class":1321}," |",[1316,3703,3704],{"class":1345}," null\n",[1316,3706,3707],{"class":1318,"line":122},[1316,3708,2251],{"class":1332},[1216,3710,3711,3712,3714],{},"並透過 ",[1220,3713,2723],{}," 處理 API 請求：",[1307,3716,3718],{"className":1309,"code":3717,"language":1311,"meta":1312,"style":1312},"import { inject } from '@angular\u002Fcore';\n\nwithMethods((store, api = inject(CartApiService)) => ({\n  async load() {\n    patchState(store, {\n      isLoading: true,\n      error: null\n    });\n\n    try {\n      const items = await api.getItems();\n\n      patchState(store, {\n        items,\n        isLoading: false\n      });\n    } catch {\n      patchState(store, {\n        error: '載入失敗',\n        isLoading: false\n      });\n    }\n  }\n}))\n",[1220,3719,3720,3733,3737,3763,3773,3781,3791,3799,3803,3807,3814,3833,3837,3843,3848,3856,3860,3870,3876,3886,3892,3896,3900,3904],{"__ignoreMap":1312},[1316,3721,3722,3724,3727,3729,3731],{"class":1318,"line":340},[1316,3723,1440],{"class":1321},[1316,3725,3726],{"class":1332}," { inject } ",[1316,3728,1446],{"class":1321},[1316,3730,1450],{"class":1449},[1316,3732,1349],{"class":1332},[1316,3734,3735],{"class":1318,"line":329},[1316,3736,1383],{"emptyLinePlaceholder":1382},[1316,3738,3739,3742,3745,3747,3749,3752,3754,3756,3759,3761],{"class":1318,"line":318},[1316,3740,3741],{"class":1478},"withMethods",[1316,3743,3744],{"class":1332},"((",[1316,3746,3041],{"class":1338},[1316,3748,1603],{"class":1332},[1316,3750,3751],{"class":1338},"api",[1316,3753,1514],{"class":1321},[1316,3755,2274],{"class":1478},[1316,3757,3758],{"class":1332},"(CartApiService)) ",[1316,3760,1612],{"class":1321},[1316,3762,2902],{"class":1332},[1316,3764,3765,3768,3771],{"class":1318,"line":134},[1316,3766,3767],{"class":1321},"  async",[1316,3769,3770],{"class":1478}," load",[1316,3772,1995],{"class":1332},[1316,3774,3775,3778],{"class":1318,"line":122},[1316,3776,3777],{"class":1478},"    patchState",[1316,3779,3780],{"class":1332},"(store, {\n",[1316,3782,3783,3786,3789],{"class":1318,"line":31},[1316,3784,3785],{"class":1332},"      isLoading: ",[1316,3787,3788],{"class":1345},"true",[1316,3790,2189],{"class":1332},[1316,3792,3793,3796],{"class":1318,"line":17},[1316,3794,3795],{"class":1332},"      error: ",[1316,3797,3798],{"class":1345},"null\n",[1316,3800,3801],{"class":1318,"line":110},[1316,3802,1905],{"class":1332},[1316,3804,3805],{"class":1318,"line":76},[1316,3806,1383],{"emptyLinePlaceholder":1382},[1316,3808,3809,3812],{"class":1318,"line":247},[1316,3810,3811],{"class":1321},"    try",[1316,3813,1333],{"class":1332},[1316,3815,3816,3818,3820,3822,3825,3828,3831],{"class":1318,"line":236},[1316,3817,1752],{"class":1321},[1316,3819,1537],{"class":1345},[1316,3821,1514],{"class":1321},[1316,3823,3824],{"class":1321}," await",[1316,3826,3827],{"class":1332}," api.",[1316,3829,3830],{"class":1478},"getItems",[1316,3832,1551],{"class":1332},[1316,3834,3835],{"class":1318,"line":225},[1316,3836,1383],{"emptyLinePlaceholder":1382},[1316,3838,3839,3841],{"class":1318,"line":214},[1316,3840,3065],{"class":1478},[1316,3842,3780],{"class":1332},[1316,3844,3845],{"class":1318,"line":203},[1316,3846,3847],{"class":1332},"        items,\n",[1316,3849,3850,3853],{"class":1318,"line":192},[1316,3851,3852],{"class":1332},"        isLoading: ",[1316,3854,3855],{"class":1345},"false\n",[1316,3857,3858],{"class":1318,"line":181},[1316,3859,3212],{"class":1332},[1316,3861,3862,3865,3868],{"class":1318,"line":170},[1316,3863,3864],{"class":1332},"    } ",[1316,3866,3867],{"class":1321},"catch",[1316,3869,1333],{"class":1332},[1316,3871,3872,3874],{"class":1318,"line":159},[1316,3873,3065],{"class":1478},[1316,3875,3780],{"class":1332},[1316,3877,3878,3881,3884],{"class":1318,"line":148},[1316,3879,3880],{"class":1332},"        error: ",[1316,3882,3883],{"class":1449},"'載入失敗'",[1316,3885,2189],{"class":1332},[1316,3887,3888,3890],{"class":1318,"line":137},[1316,3889,3852],{"class":1332},[1316,3891,3855],{"class":1345},[1316,3893,3894],{"class":1318,"line":125},[1316,3895,3212],{"class":1332},[1316,3897,3898],{"class":1318,"line":113},[1316,3899,2241],{"class":1332},[1316,3901,3902],{"class":1318,"line":101},[1316,3903,1911],{"class":1332},[1316,3905,3906],{"class":1318,"line":90},[1316,3907,3908],{"class":1332},"}))\n",[1216,3910,3911,3912,3915,3916,3919],{},"這裡假設 ",[1220,3913,3914],{},"CartApiService.getItems()"," 回傳 ",[1220,3917,3918],{},"Promise\u003CCartItem[]>","。",[1216,3921,3922,3923,3925],{},"如果其他 Store 也需要 Loading 功能，就可以透過 ",[1220,3924,3628],{}," 封裝起來。",[1216,3927,3928,3929,3347],{},"例如自己建立一個 ",[1220,3930,3931],{},"withLoading()",[1307,3933,3935],{"className":1309,"code":3934,"language":1311,"meta":1312,"style":1312},"import {\n  signalStoreFeature,\n  withState,\n  withMethods,\n  patchState\n} from '@ngrx\u002Fsignals';\n\nexport function withLoading() {\n  return signalStoreFeature(\n    withState({ isLoading: false }),\n\n    withMethods(store => ({\n      setLoading(isLoading: boolean) {\n        patchState(store, { isLoading });\n      }\n    }))\n  );\n}\n",[1220,3936,3937,3943,3948,3952,3956,3960,3970,3974,3986,3996,4009,4013,4026,4043,4051,4055,4060,4064],{"__ignoreMap":1312},[1316,3938,3939,3941],{"class":1318,"line":340},[1316,3940,1440],{"class":1321},[1316,3942,1333],{"class":1332},[1316,3944,3945],{"class":1318,"line":329},[1316,3946,3947],{"class":1332},"  signalStoreFeature,\n",[1316,3949,3950],{"class":1318,"line":318},[1316,3951,2778],{"class":1332},[1316,3953,3954],{"class":1318,"line":134},[1316,3955,2788],{"class":1332},[1316,3957,3958],{"class":1318,"line":122},[1316,3959,2793],{"class":1332},[1316,3961,3962,3964,3966,3968],{"class":1318,"line":31},[1316,3963,2798],{"class":1332},[1316,3965,1446],{"class":1321},[1316,3967,2803],{"class":1449},[1316,3969,1349],{"class":1332},[1316,3971,3972],{"class":1318,"line":17},[1316,3973,1383],{"emptyLinePlaceholder":1382},[1316,3975,3976,3978,3981,3984],{"class":1318,"line":110},[1316,3977,1322],{"class":1321},[1316,3979,3980],{"class":1321}," function",[1316,3982,3983],{"class":1478}," withLoading",[1316,3985,1995],{"class":1332},[1316,3987,3988,3991,3994],{"class":1318,"line":76},[1316,3989,3990],{"class":1321},"  return",[1316,3992,3993],{"class":1478}," signalStoreFeature",[1316,3995,1592],{"class":1332},[1316,3997,3998,4001,4004,4006],{"class":1318,"line":247},[1316,3999,4000],{"class":1478},"    withState",[1316,4002,4003],{"class":1332},"({ isLoading: ",[1316,4005,3683],{"class":1345},[1316,4007,4008],{"class":1332}," }),\n",[1316,4010,4011],{"class":1318,"line":236},[1316,4012,1383],{"emptyLinePlaceholder":1382},[1316,4014,4015,4018,4020,4022,4024],{"class":1318,"line":225},[1316,4016,4017],{"class":1478},"    withMethods",[1316,4019,1719],{"class":1332},[1316,4021,3041],{"class":1338},[1316,4023,1745],{"class":1321},[1316,4025,2902],{"class":1332},[1316,4027,4028,4031,4033,4036,4038,4041],{"class":1318,"line":214},[1316,4029,4030],{"class":1478},"      setLoading",[1316,4032,1719],{"class":1332},[1316,4034,4035],{"class":1338},"isLoading",[1316,4037,1342],{"class":1321},[1316,4039,4040],{"class":1345}," boolean",[1316,4042,1729],{"class":1332},[1316,4044,4045,4048],{"class":1318,"line":203},[1316,4046,4047],{"class":1478},"        patchState",[1316,4049,4050],{"class":1332},"(store, { isLoading });\n",[1316,4052,4053],{"class":1318,"line":192},[1316,4054,1869],{"class":1332},[1316,4056,4057],{"class":1318,"line":181},[1316,4058,4059],{"class":1332},"    }))\n",[1316,4061,4062],{"class":1318,"line":170},[1316,4063,1636],{"class":1332},[1316,4065,4066],{"class":1318,"line":159},[1316,4067,1377],{"class":1332},[1216,4069,4070,4072,4073,4075],{},[1220,4071,3628],{}," 是 NgRx 提供的函式，可以將多個 Store Feature 組合起來。這裡的 ",[1220,4074,3931],{}," 則是我們自己定義的功能。",[1216,4077,4078,4079,2112,4082,4085],{},"例如 ",[1220,4080,4081],{},"ProductStore",[1220,4083,4084],{},"UserStore"," 都需要 Loading 狀態：",[1307,4087,4089],{"className":1309,"code":4088,"language":1311,"meta":1312,"style":1312},"export const ProductStore = signalStore(\n  withState({ products: [] }),\n  withLoading()\n);\n\nexport const UserStore = signalStore(\n  withState({ users: [] }),\n  withLoading()\n);\n",[1220,4090,4091,4106,4113,4121,4125,4129,4144,4151,4157],{"__ignoreMap":1312},[1316,4092,4093,4095,4097,4100,4102,4104],{"class":1318,"line":340},[1316,4094,1322],{"class":1321},[1316,4096,2832],{"class":1321},[1316,4098,4099],{"class":1345}," ProductStore",[1316,4101,1514],{"class":1321},[1316,4103,2840],{"class":1478},[1316,4105,1592],{"class":1332},[1316,4107,4108,4110],{"class":1318,"line":329},[1316,4109,2860],{"class":1478},[1316,4111,4112],{"class":1332},"({ products: [] }),\n",[1316,4114,4115,4118],{"class":1318,"line":318},[1316,4116,4117],{"class":1478},"  withLoading",[1316,4119,4120],{"class":1332},"()\n",[1316,4122,4123],{"class":1318,"line":134},[1316,4124,3321],{"class":1332},[1316,4126,4127],{"class":1318,"line":122},[1316,4128,1383],{"emptyLinePlaceholder":1382},[1316,4130,4131,4133,4135,4138,4140,4142],{"class":1318,"line":31},[1316,4132,1322],{"class":1321},[1316,4134,2832],{"class":1321},[1316,4136,4137],{"class":1345}," UserStore",[1316,4139,1514],{"class":1321},[1316,4141,2840],{"class":1478},[1316,4143,1592],{"class":1332},[1316,4145,4146,4148],{"class":1318,"line":17},[1316,4147,2860],{"class":1478},[1316,4149,4150],{"class":1332},"({ users: [] }),\n",[1316,4152,4153,4155],{"class":1318,"line":110},[1316,4154,4117],{"class":1478},[1316,4156,4120],{"class":1332},[1316,4158,4159],{"class":1318,"line":76},[1316,4160,3321],{"class":1332},[1216,4162,4163,4164,4166,4167,4170],{},"兩個 Store 都會擁有 ",[1220,4165,4035],{}," State 與 ",[1220,4168,4169],{},"setLoading()"," Method，不需要各自重新實作。",[1294,4172,4174],{"id":4173},"rxmethod-處理非同步操作","rxMethod() 處理非同步操作",[1216,4176,4177,4178,4180],{},"SignalStore 也提供 ",[1220,4179,3614],{},"，讓我們可以在 Store 中搭配 RxJS Operator 處理非同步操作。",[1216,4182,4183],{},"例如商品搜尋，希望使用者停止輸入 300ms 後才發送 API，並在搜尋新關鍵字時取消前一次尚未完成的請求。",[1307,4185,4187],{"className":1309,"code":4186,"language":1311,"meta":1312,"style":1312},"import { inject } from '@angular\u002Fcore';\nimport { signalStore, withMethods } from '@ngrx\u002Fsignals';\nimport { rxMethod } from '@ngrx\u002Fsignals\u002Frxjs-interop';\nimport {\n  pipe,\n  debounceTime,\n  distinctUntilChanged,\n  filter,\n  switchMap\n} from 'rxjs';\n\nexport const ProductSearchStore = signalStore(\n  { providedIn: 'root' },\n\n  withMethods((store, api = inject(ProductApiService)) => ({\n    search: rxMethod\u003Cstring>(\n      pipe(\n        debounceTime(300),\n        distinctUntilChanged(),\n        filter(keyword => keyword.trim().length > 0),\n        switchMap(keyword => api.search(keyword))\n      )\n    )\n  }))\n);\n",[1220,4188,4189,4201,4214,4228,4234,4239,4244,4249,4254,4259,4270,4274,4289,4297,4301,4324,4340,4347,4360,4368,4399,4418,4422,4426,4430],{"__ignoreMap":1312},[1316,4190,4191,4193,4195,4197,4199],{"class":1318,"line":340},[1316,4192,1440],{"class":1321},[1316,4194,3726],{"class":1332},[1316,4196,1446],{"class":1321},[1316,4198,1450],{"class":1449},[1316,4200,1349],{"class":1332},[1316,4202,4203,4205,4208,4210,4212],{"class":1318,"line":329},[1316,4204,1440],{"class":1321},[1316,4206,4207],{"class":1332}," { signalStore, withMethods } ",[1316,4209,1446],{"class":1321},[1316,4211,2803],{"class":1449},[1316,4213,1349],{"class":1332},[1316,4215,4216,4218,4221,4223,4226],{"class":1318,"line":318},[1316,4217,1440],{"class":1321},[1316,4219,4220],{"class":1332}," { rxMethod } ",[1316,4222,1446],{"class":1321},[1316,4224,4225],{"class":1449}," '@ngrx\u002Fsignals\u002Frxjs-interop'",[1316,4227,1349],{"class":1332},[1316,4229,4230,4232],{"class":1318,"line":134},[1316,4231,1440],{"class":1321},[1316,4233,1333],{"class":1332},[1316,4235,4236],{"class":1318,"line":122},[1316,4237,4238],{"class":1332},"  pipe,\n",[1316,4240,4241],{"class":1318,"line":31},[1316,4242,4243],{"class":1332},"  debounceTime,\n",[1316,4245,4246],{"class":1318,"line":17},[1316,4247,4248],{"class":1332},"  distinctUntilChanged,\n",[1316,4250,4251],{"class":1318,"line":110},[1316,4252,4253],{"class":1332},"  filter,\n",[1316,4255,4256],{"class":1318,"line":76},[1316,4257,4258],{"class":1332},"  switchMap\n",[1316,4260,4261,4263,4265,4268],{"class":1318,"line":247},[1316,4262,2798],{"class":1332},[1316,4264,1446],{"class":1321},[1316,4266,4267],{"class":1449}," 'rxjs'",[1316,4269,1349],{"class":1332},[1316,4271,4272],{"class":1318,"line":236},[1316,4273,1383],{"emptyLinePlaceholder":1382},[1316,4275,4276,4278,4280,4283,4285,4287],{"class":1318,"line":225},[1316,4277,1322],{"class":1321},[1316,4279,2832],{"class":1321},[1316,4281,4282],{"class":1345}," ProductSearchStore",[1316,4284,1514],{"class":1321},[1316,4286,2840],{"class":1478},[1316,4288,1592],{"class":1332},[1316,4290,4291,4293,4295],{"class":1318,"line":214},[1316,4292,2847],{"class":1332},[1316,4294,1485],{"class":1449},[1316,4296,2315],{"class":1332},[1316,4298,4299],{"class":1318,"line":203},[1316,4300,1383],{"emptyLinePlaceholder":1382},[1316,4302,4303,4305,4307,4309,4311,4313,4315,4317,4320,4322],{"class":1318,"line":192},[1316,4304,3036],{"class":1478},[1316,4306,3744],{"class":1332},[1316,4308,3041],{"class":1338},[1316,4310,1603],{"class":1332},[1316,4312,3751],{"class":1338},[1316,4314,1514],{"class":1321},[1316,4316,2274],{"class":1478},[1316,4318,4319],{"class":1332},"(ProductApiService)) ",[1316,4321,1612],{"class":1321},[1316,4323,2902],{"class":1332},[1316,4325,4326,4329,4332,4334,4337],{"class":1318,"line":181},[1316,4327,4328],{"class":1332},"    search: ",[1316,4330,4331],{"class":1478},"rxMethod",[1316,4333,1520],{"class":1332},[1316,4335,4336],{"class":1345},"string",[1316,4338,4339],{"class":1332},">(\n",[1316,4341,4342,4345],{"class":1318,"line":170},[1316,4343,4344],{"class":1478},"      pipe",[1316,4346,1592],{"class":1332},[1316,4348,4349,4352,4354,4357],{"class":1318,"line":159},[1316,4350,4351],{"class":1478},"        debounceTime",[1316,4353,1719],{"class":1332},[1316,4355,4356],{"class":1345},"300",[1316,4358,4359],{"class":1332},"),\n",[1316,4361,4362,4365],{"class":1318,"line":148},[1316,4363,4364],{"class":1478},"        distinctUntilChanged",[1316,4366,4367],{"class":1332},"(),\n",[1316,4369,4370,4373,4375,4378,4380,4383,4386,4388,4391,4394,4397],{"class":1318,"line":137},[1316,4371,4372],{"class":1478},"        filter",[1316,4374,1719],{"class":1332},[1316,4376,4377],{"class":1338},"keyword",[1316,4379,1745],{"class":1321},[1316,4381,4382],{"class":1332}," keyword.",[1316,4384,4385],{"class":1478},"trim",[1316,4387,1587],{"class":1332},[1316,4389,4390],{"class":1345},"length",[1316,4392,4393],{"class":1321}," >",[1316,4395,4396],{"class":1345}," 0",[1316,4398,4359],{"class":1332},[1316,4400,4401,4404,4406,4408,4410,4412,4415],{"class":1318,"line":125},[1316,4402,4403],{"class":1478},"        switchMap",[1316,4405,1719],{"class":1332},[1316,4407,4377],{"class":1338},[1316,4409,1745],{"class":1321},[1316,4411,3827],{"class":1332},[1316,4413,4414],{"class":1478},"search",[1316,4416,4417],{"class":1332},"(keyword))\n",[1316,4419,4420],{"class":1318,"line":113},[1316,4421,2956],{"class":1332},[1316,4423,4424],{"class":1318,"line":101},[1316,4425,1631],{"class":1332},[1316,4427,4428],{"class":1318,"line":90},[1316,4429,3315],{"class":1332},[1316,4431,4432],{"class":1318,"line":79},[1316,4433,3321],{"class":1332},[1216,4435,4436,4437,4440,4441,4443],{},"這個範例假設 ",[1220,4438,4439],{},"api.search()"," 回傳 Observable，先省略搜尋結果的保存與錯誤處理，讓我們專注在 ",[1220,4442,3614],{}," 的使用方式。",[1216,4445,4446],{},"其中幾個 RxJS Operator 分別負責：",[1256,4448,4449,4455,4461,4467],{},[1259,4450,4451,4454],{},[1220,4452,4453],{},"debounceTime(300)","：停止輸入 300ms 後才搜尋。",[1259,4456,4457,4460],{},[1220,4458,4459],{},"distinctUntilChanged()","：避免連續處理相同的關鍵字。",[1259,4462,4463,4466],{},[1220,4464,4465],{},"filter()","：忽略空白關鍵字。",[1259,4468,4469,4472],{},[1220,4470,4471],{},"switchMap()","：開始新搜尋時，取消訂閱前一次尚未完成的請求。",[1216,4474,4475,4477],{},[1220,4476,3614],{}," 比較特別的地方，是它不只能接收一般的值，也能直接接收 Signal。",[1216,4479,4480,4481,4483],{},"例如 Component 使用 ",[1220,4482,1222],{}," 管理搜尋關鍵字：",[1307,4485,4487],{"className":1309,"code":4486,"language":1311,"meta":1312,"style":1312},"import { Component, inject, signal } from '@angular\u002Fcore';\nimport { ProductSearchStore } from '.\u002Fproduct-search.store';\n\n@Component({\n  selector: 'app-product-search',\n  template: `\n    \u003Cinput\n      [value]=\"keyword()\"\n      (input)=\"keyword.set($any($event.target).value)\"\n      placeholder=\"搜尋商品\"\n    \u002F>\n  `\n})\nexport class ProductSearch {\n  readonly store = inject(ProductSearchStore);\n  readonly keyword = signal('');\n\n  constructor() {\n    this.store.search(this.keyword);\n  }\n}\n",[1220,4488,4489,4502,4516,4520,4528,4537,4543,4548,4553,4558,4563,4568,4572,4576,4587,4600,4618,4622,4629,4645,4649],{"__ignoreMap":1312},[1316,4490,4491,4493,4496,4498,4500],{"class":1318,"line":340},[1316,4492,1440],{"class":1321},[1316,4494,4495],{"class":1332}," { Component, inject, signal } ",[1316,4497,1446],{"class":1321},[1316,4499,1450],{"class":1449},[1316,4501,1349],{"class":1332},[1316,4503,4504,4506,4509,4511,4514],{"class":1318,"line":329},[1316,4505,1440],{"class":1321},[1316,4507,4508],{"class":1332}," { ProductSearchStore } ",[1316,4510,1446],{"class":1321},[1316,4512,4513],{"class":1449}," '.\u002Fproduct-search.store'",[1316,4515,1349],{"class":1332},[1316,4517,4518],{"class":1318,"line":318},[1316,4519,1383],{"emptyLinePlaceholder":1382},[1316,4521,4522,4524,4526],{"class":1318,"line":134},[1316,4523,1475],{"class":1332},[1316,4525,2175],{"class":1478},[1316,4527,2178],{"class":1332},[1316,4529,4530,4532,4535],{"class":1318,"line":122},[1316,4531,2183],{"class":1332},[1316,4533,4534],{"class":1449},"'app-product-search'",[1316,4536,2189],{"class":1332},[1316,4538,4539,4541],{"class":1318,"line":31},[1316,4540,2194],{"class":1332},[1316,4542,2197],{"class":1449},[1316,4544,4545],{"class":1318,"line":17},[1316,4546,4547],{"class":1449},"    \u003Cinput\n",[1316,4549,4550],{"class":1318,"line":110},[1316,4551,4552],{"class":1449},"      [value]=\"keyword()\"\n",[1316,4554,4555],{"class":1318,"line":76},[1316,4556,4557],{"class":1449},"      (input)=\"keyword.set($any($event.target).value)\"\n",[1316,4559,4560],{"class":1318,"line":247},[1316,4561,4562],{"class":1449},"      placeholder=\"搜尋商品\"\n",[1316,4564,4565],{"class":1318,"line":236},[1316,4566,4567],{"class":1449},"    \u002F>\n",[1316,4569,4570],{"class":1318,"line":225},[1316,4571,2246],{"class":1449},[1316,4573,4574],{"class":1318,"line":214},[1316,4575,2251],{"class":1332},[1316,4577,4578,4580,4582,4585],{"class":1318,"line":203},[1316,4579,1322],{"class":1321},[1316,4581,1495],{"class":1321},[1316,4583,4584],{"class":1328}," ProductSearch",[1316,4586,1333],{"class":1332},[1316,4588,4589,4591,4593,4595,4597],{"class":1318,"line":192},[1316,4590,1534],{"class":1321},[1316,4592,2269],{"class":1338},[1316,4594,1514],{"class":1321},[1316,4596,2274],{"class":1478},[1316,4598,4599],{"class":1332},"(ProductSearchStore);\n",[1316,4601,4602,4604,4607,4609,4611,4613,4616],{"class":1318,"line":181},[1316,4603,1534],{"class":1321},[1316,4605,4606],{"class":1338}," keyword",[1316,4608,1514],{"class":1321},[1316,4610,1517],{"class":1478},[1316,4612,1719],{"class":1332},[1316,4614,4615],{"class":1449},"''",[1316,4617,3321],{"class":1332},[1316,4619,4620],{"class":1318,"line":170},[1316,4621,1383],{"emptyLinePlaceholder":1382},[1316,4623,4624,4627],{"class":1318,"line":159},[1316,4625,4626],{"class":1321},"  constructor",[1316,4628,1995],{"class":1332},[1316,4630,4631,4633,4636,4638,4640,4642],{"class":1318,"line":148},[1316,4632,1578],{"class":1345},[1316,4634,4635],{"class":1332},".store.",[1316,4637,4414],{"class":1478},[1316,4639,1719],{"class":1332},[1316,4641,1128],{"class":1345},[1316,4643,4644],{"class":1332},".keyword);\n",[1316,4646,4647],{"class":1318,"line":137},[1316,4648,1911],{"class":1332},[1316,4650,4651],{"class":1318,"line":125},[1316,4652,1377],{"class":1332},[1216,4654,4655,4656,4659,4660,4663],{},"這裡要注意 ",[1220,4657,4658],{},"this.keyword()"," 與 ",[1220,4661,4662],{},"this.keyword"," 的差別：",[1307,4665,4667],{"className":1309,"code":4666,"language":1311,"meta":1312,"style":1312},"this.store.search(this.keyword()); \u002F\u002F 傳入目前的字串值\n\nthis.store.search(this.keyword);   \u002F\u002F 傳入 Signal 本身\n",[1220,4668,4669,4692,4696],{"__ignoreMap":1312},[1316,4670,4671,4673,4675,4677,4679,4681,4683,4685,4688],{"class":1318,"line":340},[1316,4672,1128],{"class":1345},[1316,4674,4635],{"class":1332},[1316,4676,4414],{"class":1478},[1316,4678,1719],{"class":1332},[1316,4680,1128],{"class":1345},[1316,4682,1581],{"class":1332},[1316,4684,4377],{"class":1478},[1316,4686,4687],{"class":1332},"()); ",[1316,4689,4691],{"class":4690},"sDN9O","\u002F\u002F 傳入目前的字串值\n",[1316,4693,4694],{"class":1318,"line":329},[1316,4695,1383],{"emptyLinePlaceholder":1382},[1316,4697,4698,4700,4702,4704,4706,4708,4711],{"class":1318,"line":318},[1316,4699,1128],{"class":1345},[1316,4701,4635],{"class":1332},[1316,4703,4414],{"class":1478},[1316,4705,1719],{"class":1332},[1316,4707,1128],{"class":1345},[1316,4709,4710],{"class":1332},".keyword);   ",[1316,4712,4713],{"class":4690},"\u002F\u002F 傳入 Signal 本身\n",[1216,4715,4716,4717,4719,4720,4722],{},"第一種只會將當下的字串送進 ",[1220,4718,3614],{},"，後續 ",[1220,4721,4377],{}," 改變不會自動觸發搜尋。",[1216,4724,4725,4726,4728,4729,4731,4732,4735],{},"第二種則會讓 ",[1220,4727,3614],{}," 追蹤 Signal。使用者輸入文字時，",[1220,4730,4377],{}," 的新值就會送進 RxJS Pipeline，不需要另外使用 ",[1220,4733,4734],{},"effect()"," 監聽，也不用每次手動呼叫搜尋方法。",[1216,4737,4738,4739,4741],{},"傳入 Signal 時也會讀取初始值，因此範例透過 ",[1220,4740,4465],{}," 忽略空字串。實際使用時，如果需要清空舊搜尋結果，或處理 API 失敗的情況，也要另外補上對應邏輯。",[1294,4743,4745],{"id":4744},"什麼時候該使用-ngrx-signalstore","什麼時候該使用 NgRx SignalStore？",[1216,4747,4748],{},"我覺得不需要單純根據專案大小決定，而是看狀態管理本身有多複雜。",[1216,4750,4751],{},"如果只是購物車、側邊欄開關、使用者偏好等簡單狀態，原生 Signal 搭配 Service 通常就很夠用。",[1216,4753,4754],{},"但如果同一個功能需要管理大量狀態、非同步請求與錯誤處理，或是多個 Store 開始出現重複的功能，那麼 SignalStore 提供的 Feature 設計就會比較方便。",[1245,4756,4757],{"id":4757},"本日小結",[1216,4759,4760],{},"這次使用原生 Signal 完成了一個購物車 Store。搭配 Service 與 Dependency Injection，就能讓不同 Component 共用狀態，不一定要另外引入 NgRx。",[1216,4762,4763,4764,1236,4766,4659,4768,4770,4771,4773],{},"NgRx SignalStore 同樣建立在 Angular Signal 之上，但透過 ",[1220,4765,2703],{},[1220,4767,2713],{},[1220,4769,2723],{},"，讓 State、衍生資料與操作方法有各自的位置，也能使用 ",[1220,4772,2733],{}," 更新狀態。",[1216,4775,4776,4777,4779,4780,4782],{},"除了基本的 Store 功能，",[1220,4778,3628],{}," 可以將重複的邏輯封裝起來，",[1220,4781,3614],{}," 則能搭配 RxJS 處理搜尋、延遲請求等非同步操作。如果專案中有許多類似需求，這些功能就會比較實用。",[1216,4784,4785],{},"不過，使用 SignalStore 不代表一定比較好。如果原生 Signal 已經能滿足需求，就不需要特別增加 Library。",[1294,4787,4788],{"id":4788},"資料來源",[1256,4790,4791,4800,4807,4814,4821],{},[1259,4792,4793],{},[4794,4795,4799],"a",{"href":4796,"rel":4797},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals#converting-writable-signals-to-readonly",[4798],"nofollow","Angular Signals 官方文件：Converting writable signals to readonly",[1259,4801,4802],{},[4794,4803,4806],{"href":4804,"rel":4805},"https:\u002F\u002Fangular.dev\u002Ftutorials\u002Fsignals\u002F7-using-signals-with-services",[4798],"Angular 官方教學：Using signals with services",[1259,4808,4809],{},[4794,4810,4813],{"href":4811,"rel":4812},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Fsignal-store",[4798],"NgRx 官方文件：SignalStore",[1259,4815,4816],{},[4794,4817,4820],{"href":4818,"rel":4819},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Fsignal-store\u002Fcustom-store-features",[4798],"NgRx 官方文件：Custom Store Features",[1259,4822,4823],{},[4794,4824,4827],{"href":4825,"rel":4826},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Frxjs-integration",[4798],"NgRx 官方文件：RxJS Integration",[4829,4830,4831],"style",{},"html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sMOND, html code.shiki .sMOND{--shiki-default:#6F42C1;--shiki-dark:#6CB6FF}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 .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}",{"title":1312,"searchDepth":134,"depth":134,"links":4833},[4834,4838,4842,4847],{"id":1247,"depth":329,"text":1248,"children":4835},[4836,4837],{"id":1296,"depth":318,"text":1297},{"id":2119,"depth":318,"text":2120},{"id":2643,"depth":329,"text":2644,"children":4839},[4840,4841],{"id":2662,"depth":318,"text":2663},{"id":3389,"depth":318,"text":3390},{"id":3510,"depth":329,"text":3511,"children":4843},[4844,4845,4846],{"id":3647,"depth":318,"text":3648},{"id":4173,"depth":318,"text":4174},{"id":4744,"depth":318,"text":4745},{"id":4757,"depth":329,"text":4757,"children":4848},[4849],{"id":4788,"depth":318,"text":4788},[11,13],"md",{},"---\ntitle: \"Day 25：Angular SignalStore，原生 Signal 不夠用嗎？\"\ndate: 2026-10-09 21:28:45\ntags:\n  - \"iThome 鐵人賽\"\n  - \"2026 鐵人賽\"\n  - \"Angular\"\ncategories:\n  - \"iThome 鐵人賽\"\n  - \"Angular\"\ndescription: \"在 Angular 開發中，如果資料只會被單一 Component 使用，通常可以直接透過 signal() 管理。但當多個 Component 需要共用同一份資料時，就得考慮狀態應該放在哪裡。\"\ncanonical: https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422784\nseries:\n  name: \"2026 iThome 鐵人賽：Angular 22 Signal 進化論\"\n  url: https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F9939\n  day: 25\n---\n\n在 Angular 開發中，如果資料只會被單一 Component 使用，通常可以直接透過 `signal()` 管理。但當多個 Component 需要共用同一份資料時，就得考慮狀態應該放在哪裡。\n\n例如電商網站的購物車，商品列表需要新增商品、Navbar 需要顯示購物車數量，購物車頁面則需要計算總金額。如果每個 Component 都管理自己的資料，就得額外處理彼此之間的同步。\n\n過去我們可能會使用 RxJS `BehaviorSubject`，或引入 NgRx 這類狀態管理工具。但現在 Angular 已經提供 `signal()`、`computed()` 與 Dependency Injection（DI），是不是也能直接用這些功能建立 Store？\n\n這篇會先使用原生 Signal 實作購物車，再改用 NgRx SignalStore 完成相同功能，看看兩種寫法有什麼差別。\n\n## 使用 Signal 建立自己的 Store\n\nStore 不一定要透過 Library 才能實作，可以先把它理解成一個集中管理狀態的地方。\n\n以購物車為例，Store 需要管理三種內容：\n\n- **State**：保存購物車中的商品。\n- **Derived State**：根據商品清單計算數量與總金額。\n- **Methods**：提供新增、刪除與清空購物車的方法。\n\n使用 Angular 原生的 `signal()`、`computed()` 與 Service，就能完成這些功能。\n\n![跨 Component 共用狀態：ProductList、Navbar、Cart 共用同一個 CartStore](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791552129\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2025%EF%BC%9AAngular%20SignalStore%EF%BC%8C%E5%8E%9F%E7%94%9F%20Signal%20%E4%B8%8D%E5%A4%A0%E7%94%A8%E5%97%8E%EF%BC%9F\u002Fshared-cart-store.png)\n\n### 建立 Cart Store\n\n先定義商品資料結構：\n\n`cart.types.ts`\n\n```ts\nexport interface Product {\n  id: number;\n  name: string;\n  price: number;\n}\n\nexport interface CartItem extends Product {\n  quantity: number;\n}\n```\n\n`CartItem` 除了商品資訊，還會透過 `quantity` 記錄購買數量。\n\n`cart.store.ts`\n\n```ts\nimport { Injectable, signal, computed } from '@angular\u002Fcore';\nimport { CartItem, Product } from '.\u002Fcart.types';\n\n@Injectable({ providedIn: 'root' })\nexport class CartStore {\n  private readonly _items = signal\u003CCartItem[]>([]);\n\n  readonly items = this._items.asReadonly();\n\n  readonly count = computed(() =>\n    this.items().reduce(\n      (sum, item) => sum + item.quantity,\n      0\n    )\n  );\n\n  readonly total = computed(() =>\n    this.items().reduce(\n      (sum, item) => sum + item.price * item.quantity,\n      0\n    )\n  );\n\n  add(product: Product) {\n    this._items.update(items => {\n      const existing = items.find(\n        item => item.id === product.id\n      );\n\n      if (existing) {\n        return items.map(item =>\n          item.id === product.id\n            ? { ...item, quantity: item.quantity + 1 }\n            : item\n        );\n      }\n\n      return [...items, { ...product, quantity: 1 }];\n    });\n  }\n\n  remove(id: number) {\n    this._items.update(items =>\n      items.filter(item => item.id !== id)\n    );\n  }\n\n  clear() {\n    this._items.set([]);\n  }\n}\n```\n\n這個 Service 已經具備基本 Store 的功能。`signal()` 保存商品資料、`computed()` 計算數量與總金額，Methods 則負責修改購物車內容。\n\n其中比較需要注意的是狀態封裝：\n\n```ts\nprivate readonly _items = signal\u003CCartItem[]>([]);\n\nreadonly items = this._items.asReadonly();\n```\n\n將 `WritableSignal` 設為 private，再透過 `asReadonly()` 對外提供唯讀 Signal。這樣 Component 可以讀取 `items()`，但不能直接呼叫 `set()` 或 `update()`，而是要透過 Store 提供的方法修改。\n\n不過，`asReadonly()` 不會深層凍結物件，因此仍然需要避免直接修改陣列內的商品資料。\n\n新增商品或調整數量時，`update()` 會回傳新的陣列，而不是直接修改原本的資料。當 `items` 更新後，`count` 和 `total` 也會依據最新的商品資料計算。\n\n### 跨 Component 共用狀態\n\n有了 Store 之後，不同 Component 就能直接注入同一個 `CartStore`，不需要透過父元件逐層傳遞購物車資料。\n\n`product-list.ts`\n\n```ts\nimport { Component, inject } from '@angular\u002Fcore';\nimport { CartStore } from '.\u002Fcart.store';\n\n@Component({\n  selector: 'app-product-list',\n  template: `\n    @for (product of products; track product.id) {\n      \u003Cdiv>\n        \u003Cspan>{{ product.name }} - NT$ {{ product.price }}\u003C\u002Fspan>\n\n        \u003Cbutton (click)=\"store.add(product)\">\n          加入購物車\n        \u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    }\n  `\n})\nexport class ProductList {\n  readonly store = inject(CartStore);\n\n  readonly products = [\n    { id: 1, name: '鍵盤', price: 1200 },\n    { id: 2, name: '滑鼠', price: 800 }\n  ];\n}\n```\n\nNavbar 只需要讀取購物車數量：\n\n`navbar.ts`\n\n```ts\nimport { Component, inject } from '@angular\u002Fcore';\nimport { CartStore } from '.\u002Fcart.store';\n\n@Component({\n  selector: 'app-navbar',\n  template: `\n    \u003Cnav>\n      購物車 ({{ store.count() }})\n    \u003C\u002Fnav>\n  `\n})\nexport class Navbar {\n  readonly store = inject(CartStore);\n}\n```\n\n購物車頁面則顯示商品清單、總金額，並提供刪除與清空功能：\n\n`cart.ts`\n\n```ts\nimport { Component, inject } from '@angular\u002Fcore';\nimport { CartStore } from '.\u002Fcart.store';\n\n@Component({\n  selector: 'app-cart',\n  template: `\n    @for (item of store.items(); track item.id) {\n      \u003Cdiv>\n        {{ item.name }} × {{ item.quantity }}\n\n        \u003Cbutton (click)=\"store.remove(item.id)\">\n          刪除\n        \u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    }\n\n    \u003Cp>總金額：NT$ {{ store.total() }}\u003C\u002Fp>\n\n    \u003Cbutton (click)=\"store.clear()\">\n      清空購物車\n    \u003C\u002Fbutton>\n  `\n})\nexport class Cart {\n  readonly store = inject(CartStore);\n}\n```\n\n由於 `CartStore` 設定了 `providedIn: 'root'`，這三個 Component 在沒有覆寫 Provider 的情況下，會取得同一個 Service 實體。\n\n所以當商品列表加入商品時，Navbar 的數量與購物車頁面的總金額也會跟著更新。\n\n到這裡，我們已經使用原生 Signal 完成一個簡單的購物車 Store，完全不需要 NgRx。\n\n## 改用 NgRx SignalStore\n\n既然原生 Signal 就能建立 Store，為什麼還需要 NgRx SignalStore？\n\n當 Store 開始加入更多狀態、非同步請求與操作方法時，Service 可能會越寫越大。雖然可以自行拆分，但也需要考慮各個 Store 的程式碼該怎麼組織，以及共通功能要怎麼重複使用。\n\nNgRx SignalStore 是 NgRx 提供的狀態管理工具，建立在 Angular 原生 Signal 之上。它將 State、Computed 與 Methods 拆成不同的 Feature，讓我們可以依照需求組合 Store。\n\n這裡要注意，NgRx SignalStore 與傳統 NgRx Store 是不同的方案。傳統 NgRx Store 主要使用 Action、Reducer 等架構，而 SignalStore 則以 Signal 與 Method 為核心。\n\n這次會使用 NgRx SignalStore 22，搭配 Angular 22 進行實作。\n\n### 建立 NgRx SignalStore\n\nNgRx SignalStore 主要提供以下 API：\n\n| API | 用途 |\n| --- | --- |\n| `signalStore()` | 建立 Store |\n| `withState()` | 定義初始狀態 |\n| `withComputed()` | 建立衍生狀態 |\n| `withMethods()` | 定義操作方法 |\n| `patchState()` | 更新 Store 狀態 |\n\n我們使用相同的購物車需求重新實作。\n\n`ngrx-cart.store.ts`\n\n```ts\nimport { computed } from '@angular\u002Fcore';\nimport {\n  signalStore,\n  withState,\n  withComputed,\n  withMethods,\n  patchState\n} from '@ngrx\u002Fsignals';\n\nimport { CartItem, Product } from '.\u002Fcart.types';\n\nexport const NgRxCartStore = signalStore(\n  { providedIn: 'root' },\n\n  withState({\n    items: [] as CartItem[]\n  }),\n\n  withComputed(({ items }) => ({\n    count: computed(() =>\n      items().reduce(\n        (sum, item) => sum + item.quantity,\n        0\n      )\n    ),\n\n    total: computed(() =>\n      items().reduce(\n        (sum, item) => sum + item.price * item.quantity,\n        0\n      )\n    )\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購物車的功能沒有改變，但 State、Computed 與 Methods 有了各自的位置。\n\n原本需要自行建立 private `signal()`，再透過 `asReadonly()` 對外提供資料；SignalStore 則使用 `withState()` 定義狀態，並自動提供對應的唯讀 Signal。\n\n修改狀態時，原本使用的 `set()` 或 `update()`，現在則改成 NgRx 提供的 `patchState()`：\n\n```ts\npatchState(store, { items: [] });\n```\n\n`patchState()` 可以直接指定新值，也能根據目前的 State 計算更新結果，而且只會修改指定的屬性。\n\n它通常會搭配 `withMethods()` 使用，讓 Component 透過 `add()`、`remove()` 等方法操作狀態。SignalStore 預設也會保護 State，外部無法直接對受保護的 Store 呼叫 `patchState()`，避免狀態被隨意修改。\n\n![原生 Signal + Service 與 NgRx SignalStore 的對應關係](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791552133\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2025%EF%BC%9AAngular%20SignalStore%EF%BC%8C%E5%8E%9F%E7%94%9F%20Signal%20%E4%B8%8D%E5%A4%A0%E7%94%A8%E5%97%8E%EF%BC%9F\u002Fnative-signal-vs-signalstore.png)\n\n### Component 需要調整嗎？\n\n如果讓前面的 Navbar 改用 NgRx SignalStore，只需要注入新的 Store：\n\n```ts\nimport { Component, inject } from '@angular\u002Fcore';\nimport { NgRxCartStore } from '.\u002Fngrx-cart.store';\n\n@Component({\n  selector: 'app-navbar',\n  template: `\n    \u003Cnav>\n      購物車 ({{ store.count() }})\n    \u003C\u002Fnav>\n  `\n})\nexport class Navbar {\n  readonly store = inject(NgRxCartStore);\n}\n```\n\nTemplate 仍然透過 `store.count()` 讀取資料，因為 SignalStore 對外提供的也是 Signal。\n\n這次兩個 Store 剛好使用相同的 Property 與 Method 名稱，因此 Component 的寫法很接近。不過，如果要在實際專案中替換 Store，仍然需要確認原本的功能與介面是否一致。\n\n## 原生 Signal 與 NgRx SignalStore 差在哪裡？\n\n從剛剛的購物車實作可以發現，兩種方式都能完成相同的功能，而且使用 SignalStore 不一定能減少程式碼。\n\nSignalStore 比較大的差別，是它提供了一套可以組合與重複使用的 Store 結構。\n\n| 比較項目 | 原生 Signal + Service | NgRx SignalStore |\n| --- | --- | --- |\n| 狀態管理 | `signal()` | `withState()` |\n| 衍生狀態 | `computed()` | `withComputed()` |\n| 修改狀態 | `set()`、`update()` | `patchState()` |\n| 操作方法 | Service Methods | `withMethods()` |\n| 狀態封裝 | private、`asReadonly()` | 預設保護 State |\n| 非同步操作 | Promise、RxJS、Resource 等 | Promise、`rxMethod()` 等 |\n| 共通邏輯擴充 | Service、函式或組合設計 | `signalStoreFeature()` |\n| 額外依賴 | 無 | `@ngrx\u002Fsignals` |\n\n以目前的購物車來說，原生 Signal 就很夠用了。但如果之後需要加入 API 請求、Loading、Error，或是多個 Store 都需要相同的功能，就可以考慮 SignalStore 的 Feature 設計。\n\n### NgRx SignalStore 的擴充能力\n\n例如購物車需要從後端載入資料，可以在 Store 中加入 Loading 與 Error 狀態：\n\n```ts\nwithState({\n  items: [] as CartItem[],\n  isLoading: false,\n  error: null as string | null\n})\n```\n\n並透過 `withMethods()` 處理 API 請求：\n\n```ts\nimport { inject } from '@angular\u002Fcore';\n\nwithMethods((store, api = inject(CartApiService)) => ({\n  async load() {\n    patchState(store, {\n      isLoading: true,\n      error: null\n    });\n\n    try {\n      const items = await api.getItems();\n\n      patchState(store, {\n        items,\n        isLoading: false\n      });\n    } catch {\n      patchState(store, {\n        error: '載入失敗',\n        isLoading: false\n      });\n    }\n  }\n}))\n```\n\n這裡假設 `CartApiService.getItems()` 回傳 `Promise\u003CCartItem[]>`。\n\n如果其他 Store 也需要 Loading 功能，就可以透過 `signalStoreFeature()` 封裝起來。\n\n例如自己建立一個 `withLoading()`：\n\n```ts\nimport {\n  signalStoreFeature,\n  withState,\n  withMethods,\n  patchState\n} from '@ngrx\u002Fsignals';\n\nexport function withLoading() {\n  return signalStoreFeature(\n    withState({ isLoading: false }),\n\n    withMethods(store => ({\n      setLoading(isLoading: boolean) {\n        patchState(store, { isLoading });\n      }\n    }))\n  );\n}\n```\n\n`signalStoreFeature()` 是 NgRx 提供的函式，可以將多個 Store Feature 組合起來。這裡的 `withLoading()` 則是我們自己定義的功能。\n\n例如 `ProductStore` 和 `UserStore` 都需要 Loading 狀態：\n\n```ts\nexport const ProductStore = signalStore(\n  withState({ products: [] }),\n  withLoading()\n);\n\nexport const UserStore = signalStore(\n  withState({ users: [] }),\n  withLoading()\n);\n```\n\n兩個 Store 都會擁有 `isLoading` State 與 `setLoading()` Method，不需要各自重新實作。\n\n### rxMethod() 處理非同步操作\n\nSignalStore 也提供 `rxMethod()`，讓我們可以在 Store 中搭配 RxJS Operator 處理非同步操作。\n\n例如商品搜尋，希望使用者停止輸入 300ms 後才發送 API，並在搜尋新關鍵字時取消前一次尚未完成的請求。\n\n```ts\nimport { inject } from '@angular\u002Fcore';\nimport { signalStore, withMethods } from '@ngrx\u002Fsignals';\nimport { rxMethod } from '@ngrx\u002Fsignals\u002Frxjs-interop';\nimport {\n  pipe,\n  debounceTime,\n  distinctUntilChanged,\n  filter,\n  switchMap\n} from 'rxjs';\n\nexport const ProductSearchStore = signalStore(\n  { providedIn: 'root' },\n\n  withMethods((store, api = inject(ProductApiService)) => ({\n    search: rxMethod\u003Cstring>(\n      pipe(\n        debounceTime(300),\n        distinctUntilChanged(),\n        filter(keyword => keyword.trim().length > 0),\n        switchMap(keyword => api.search(keyword))\n      )\n    )\n  }))\n);\n```\n\n這個範例假設 `api.search()` 回傳 Observable，先省略搜尋結果的保存與錯誤處理，讓我們專注在 `rxMethod()` 的使用方式。\n\n其中幾個 RxJS Operator 分別負責：\n\n- `debounceTime(300)`：停止輸入 300ms 後才搜尋。\n- `distinctUntilChanged()`：避免連續處理相同的關鍵字。\n- `filter()`：忽略空白關鍵字。\n- `switchMap()`：開始新搜尋時，取消訂閱前一次尚未完成的請求。\n\n`rxMethod()` 比較特別的地方，是它不只能接收一般的值，也能直接接收 Signal。\n\n例如 Component 使用 `signal()` 管理搜尋關鍵字：\n\n```ts\nimport { Component, inject, signal } from '@angular\u002Fcore';\nimport { ProductSearchStore } from '.\u002Fproduct-search.store';\n\n@Component({\n  selector: 'app-product-search',\n  template: `\n    \u003Cinput\n      [value]=\"keyword()\"\n      (input)=\"keyword.set($any($event.target).value)\"\n      placeholder=\"搜尋商品\"\n    \u002F>\n  `\n})\nexport class ProductSearch {\n  readonly store = inject(ProductSearchStore);\n  readonly keyword = signal('');\n\n  constructor() {\n    this.store.search(this.keyword);\n  }\n}\n```\n\n這裡要注意 `this.keyword()` 與 `this.keyword` 的差別：\n\n```ts\nthis.store.search(this.keyword()); \u002F\u002F 傳入目前的字串值\n\nthis.store.search(this.keyword);   \u002F\u002F 傳入 Signal 本身\n```\n\n第一種只會將當下的字串送進 `rxMethod()`，後續 `keyword` 改變不會自動觸發搜尋。\n\n第二種則會讓 `rxMethod()` 追蹤 Signal。使用者輸入文字時，`keyword` 的新值就會送進 RxJS Pipeline，不需要另外使用 `effect()` 監聽，也不用每次手動呼叫搜尋方法。\n\n傳入 Signal 時也會讀取初始值，因此範例透過 `filter()` 忽略空字串。實際使用時，如果需要清空舊搜尋結果，或處理 API 失敗的情況，也要另外補上對應邏輯。\n\n### 什麼時候該使用 NgRx SignalStore？\n\n我覺得不需要單純根據專案大小決定，而是看狀態管理本身有多複雜。\n\n如果只是購物車、側邊欄開關、使用者偏好等簡單狀態，原生 Signal 搭配 Service 通常就很夠用。\n\n但如果同一個功能需要管理大量狀態、非同步請求與錯誤處理，或是多個 Store 開始出現重複的功能，那麼 SignalStore 提供的 Feature 設計就會比較方便。\n\n## 本日小結\n\n這次使用原生 Signal 完成了一個購物車 Store。搭配 Service 與 Dependency Injection，就能讓不同 Component 共用狀態，不一定要另外引入 NgRx。\n\nNgRx SignalStore 同樣建立在 Angular Signal 之上，但透過 `withState()`、`withComputed()` 與 `withMethods()`，讓 State、衍生資料與操作方法有各自的位置，也能使用 `patchState()` 更新狀態。\n\n除了基本的 Store 功能，`signalStoreFeature()` 可以將重複的邏輯封裝起來，`rxMethod()` 則能搭配 RxJS 處理搜尋、延遲請求等非同步操作。如果專案中有許多類似需求，這些功能就會比較實用。\n\n不過，使用 SignalStore 不代表一定比較好。如果原生 Signal 已經能滿足需求，就不需要特別增加 Library。\n\n### 資料來源\n\n- [Angular Signals 官方文件：Converting writable signals to readonly](https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals#converting-writable-signals-to-readonly)\n- [Angular 官方教學：Using signals with services](https:\u002F\u002Fangular.dev\u002Ftutorials\u002Fsignals\u002F7-using-signals-with-services)\n- [NgRx 官方文件：SignalStore](https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Fsignal-store)\n- [NgRx 官方文件：Custom Store Features](https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Fsignal-store\u002Fcustom-store-features)\n- [NgRx 官方文件：RxJS Integration](https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Frxjs-integration)\n",{"title":70,"description":71},{"name":20,"url":21,"day":79},{"loc":69,"images":4857,"lastmod":72},[4858,4859],{"loc":1292},{"loc":3386},"ithome-2026-day-25","2026-10-09-ithome-2026-day-25",[11,12,13],"XX3JszuLfKLt_pPxarAAKXIsQb-q7U8n9cSP9J_7w4E",1792575170220]