[{"data":1,"prerenderedAt":3941},["ShallowReactive",2],{"all-posts":3,"post:\u002Fithome-2026-day-27":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":48,"body":1212,"canonical":54,"categories":3927,"date":50,"description":49,"draft":15,"extension":3928,"meta":3929,"navigation":1359,"ogImage":9,"path":47,"rawbody":3930,"readingTime":17,"robots":9,"schemaOrg":9,"seo":3931,"series":3932,"sitemap":3933,"slug":3937,"stem":3938,"tags":3939,"updated":9,"wordCount":53,"__hash__":3940},"posts\u002F2026-10-11-ithome-2026-day-27.md",{"type":1213,"value":1214,"toc":3907},"minimark",[1215,1236,1244,1247,1250,1255,1258,1280,1283,1290,1295,1302,1307,1492,1506,1513,1534,1541,1545,1548,1551,1556,1679,1686,1691,2120,2129,2135,2142,2149,2159,2163,2166,2176,2181,2454,2467,2470,2474,2477,2481,2486,2590,2603,2607,2613,2624,2680,2698,2703,2708,3306,3315,3327,3331,3341,3345,3351,3367,3373,3388,3393,3396,3399,3405,3409,3412,3446,3452,3493,3498,3504,3508,3514,3585,3594,3685,3688,3696,3700,3703,3810,3813,3816,3823,3826,3829,3832,3835,3848,3851,3854,3857,3903],[1216,1217,1218,1219,1223,1224,1227,1228,1231,1232,1235],"p",{},"前幾天我們使用 NgRx SignalStore 實作購物車，透過 ",[1220,1221,1222],"code",{},"withState()"," 管理商品資料、",[1220,1225,1226],{},"withComputed()"," 計算總金額，再利用 ",[1220,1229,1230],{},"withMethods()"," 與 ",[1220,1233,1234],{},"patchState()"," 更新 State。",[1216,1237,1238,1239,1243],{},"不過，在 SignalStore 出現之前，NgRx 就已經提供了另一套狀態管理方案，也就是傳統的 ",[1240,1241,1242],"strong",{},"NgRx Store","。",[1216,1245,1246],{},"NgRx Store 採用 Redux 的設計概念，透過 Action 描述事件，再交由 Reducer 更新 State。這與 SignalStore 直接呼叫 Method 的方式不同。",[1216,1248,1249],{},"今天延續購物車範例，使用傳統 NgRx Store 重新實作一次，比較兩種方式在狀態管理上的差異。",[1251,1252,1254],"h2",{"id":1253},"認識傳統-ngrx-store","認識傳統 NgRx Store",[1216,1256,1257],{},"NgRx Store 主要由三個部分組成：",[1259,1260,1261,1268,1274],"ul",{},[1262,1263,1264,1267],"li",{},[1240,1265,1266],{},"Action","：描述發生了什麼事件。",[1262,1269,1270,1273],{},[1240,1271,1272],{},"Reducer","：根據 Action 與目前的 State，產生新的 State。",[1262,1275,1276,1279],{},[1240,1277,1278],{},"Selector","：取得 State 中需要的資料，或計算衍生資料。",[1216,1281,1282],{},"例如使用者點擊「加入購物車」，Component 會發送 Action，再由 Reducer 更新購物車 State，Selector 則負責取得畫面需要的資料。",[1216,1284,1285],{},[1286,1287],"img",{"alt":1288,"src":1289},"NgRx Store 的 Action、Reducer、State 與 Selector 關係圖","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791719451\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2027%EF%BC%9ANgRx%20Store%20vs%20SignalStore%EF%BC%8C%E5%BE%9E%20Action%E3%80%81Reducer%20%E5%88%B0%E7%9B%B4%E6%8E%A5%E6%9B%B4%E6%96%B0%20State\u002Fngrx-store-overview.png",[1291,1292,1294],"h3",{"id":1293},"定義-action","定義 Action",[1216,1296,1297,1298,1301],{},"Action 用來描述 Application 中發生的事件。NgRx 提供 ",[1220,1299,1300],{},"createAction()"," 定義事件名稱，以及需要攜帶的資料。",[1216,1303,1304],{},[1220,1305,1306],{},"cart.actions.ts",[1308,1309,1314],"pre",{"className":1310,"code":1311,"language":1312,"meta":1313,"style":1313},"language-ts shiki shiki-themes github-light github-dark-dimmed","import { createAction, props } from '@ngrx\u002Fstore';\nimport type { Product } from '.\u002Fcart.types';\n\nexport const addProduct = createAction(\n  '[Cart] Add Product',\n  props\u003C{ product: Product }>()\n);\n\nexport const removeProduct = createAction(\n  '[Cart] Remove Product',\n  props\u003C{ id: number }>()\n);\n\nexport const clearCart = createAction(\n  '[Cart] Clear Cart'\n);\n","ts","",[1220,1315,1316,1338,1355,1361,1383,1391,1413,1418,1422,1437,1444,1460,1464,1468,1483,1488],{"__ignoreMap":1313},[1317,1318,1320,1324,1328,1331,1335],"span",{"class":1319,"line":340},"line",[1317,1321,1323],{"class":1322},"s7YZ4","import",[1317,1325,1327],{"class":1326},"sYgZi"," { createAction, props } ",[1317,1329,1330],{"class":1322},"from",[1317,1332,1334],{"class":1333},"s-HuK"," '@ngrx\u002Fstore'",[1317,1336,1337],{"class":1326},";\n",[1317,1339,1340,1342,1345,1348,1350,1353],{"class":1319,"line":329},[1317,1341,1323],{"class":1322},[1317,1343,1344],{"class":1322}," type",[1317,1346,1347],{"class":1326}," { Product } ",[1317,1349,1330],{"class":1322},[1317,1351,1352],{"class":1333}," '.\u002Fcart.types'",[1317,1354,1337],{"class":1326},[1317,1356,1357],{"class":1319,"line":318},[1317,1358,1360],{"emptyLinePlaceholder":1359},true,"\n",[1317,1362,1363,1366,1369,1373,1376,1380],{"class":1319,"line":134},[1317,1364,1365],{"class":1322},"export",[1317,1367,1368],{"class":1322}," const",[1317,1370,1372],{"class":1371},"sviXB"," addProduct",[1317,1374,1375],{"class":1322}," =",[1317,1377,1379],{"class":1378},"sPO5f"," createAction",[1317,1381,1382],{"class":1326},"(\n",[1317,1384,1385,1388],{"class":1319,"line":122},[1317,1386,1387],{"class":1333},"  '[Cart] Add Product'",[1317,1389,1390],{"class":1326},",\n",[1317,1392,1393,1396,1399,1403,1406,1410],{"class":1319,"line":31},[1317,1394,1395],{"class":1378},"  props",[1317,1397,1398],{"class":1326},"\u003C{ ",[1317,1400,1402],{"class":1401},"stnAF","product",[1317,1404,1405],{"class":1322},":",[1317,1407,1409],{"class":1408},"sOLd2"," Product",[1317,1411,1412],{"class":1326}," }>()\n",[1317,1414,1415],{"class":1319,"line":17},[1317,1416,1417],{"class":1326},");\n",[1317,1419,1420],{"class":1319,"line":110},[1317,1421,1360],{"emptyLinePlaceholder":1359},[1317,1423,1424,1426,1428,1431,1433,1435],{"class":1319,"line":76},[1317,1425,1365],{"class":1322},[1317,1427,1368],{"class":1322},[1317,1429,1430],{"class":1371}," removeProduct",[1317,1432,1375],{"class":1322},[1317,1434,1379],{"class":1378},[1317,1436,1382],{"class":1326},[1317,1438,1439,1442],{"class":1319,"line":247},[1317,1440,1441],{"class":1333},"  '[Cart] Remove Product'",[1317,1443,1390],{"class":1326},[1317,1445,1446,1448,1450,1453,1455,1458],{"class":1319,"line":236},[1317,1447,1395],{"class":1378},[1317,1449,1398],{"class":1326},[1317,1451,1452],{"class":1401},"id",[1317,1454,1405],{"class":1322},[1317,1456,1457],{"class":1371}," number",[1317,1459,1412],{"class":1326},[1317,1461,1462],{"class":1319,"line":225},[1317,1463,1417],{"class":1326},[1317,1465,1466],{"class":1319,"line":214},[1317,1467,1360],{"emptyLinePlaceholder":1359},[1317,1469,1470,1472,1474,1477,1479,1481],{"class":1319,"line":203},[1317,1471,1365],{"class":1322},[1317,1473,1368],{"class":1322},[1317,1475,1476],{"class":1371}," clearCart",[1317,1478,1375],{"class":1322},[1317,1480,1379],{"class":1378},[1317,1482,1382],{"class":1326},[1317,1484,1485],{"class":1319,"line":192},[1317,1486,1487],{"class":1333},"  '[Cart] Clear Cart'\n",[1317,1489,1490],{"class":1319,"line":181},[1317,1491,1417],{"class":1326},[1216,1493,1494,1497,1498,1501,1502,1505],{},[1220,1495,1496],{},"props()"," 用來定義 Action 需要攜帶的資料。例如 ",[1220,1499,1500],{},"addProduct"," 需要提供商品，而 ",[1220,1503,1504],{},"clearCart"," 不需要額外參數。",[1216,1507,1508,1509,1512],{},"定義完成後，可以透過 ",[1220,1510,1511],{},"dispatch()"," 將 Action 發送給 Store：",[1308,1514,1516],{"className":1310,"code":1515,"language":1312,"meta":1313,"style":1313},"store.dispatch(addProduct({ product }));\n",[1220,1517,1518],{"__ignoreMap":1313},[1317,1519,1520,1523,1526,1529,1531],{"class":1319,"line":340},[1317,1521,1522],{"class":1326},"store.",[1317,1524,1525],{"class":1378},"dispatch",[1317,1527,1528],{"class":1326},"(",[1317,1530,1500],{"class":1378},[1317,1532,1533],{"class":1326},"({ product }));\n",[1216,1535,1536,1537,1540],{},"這裡的 ",[1220,1538,1539],{},"addProduct()"," 只負責建立 Action 物件，真正的狀態更新會交由 Reducer 處理。",[1291,1542,1544],{"id":1543},"使用-reducer-更新-state","使用 Reducer 更新 State",[1216,1546,1547],{},"Reducer 是一個 Pure Function（純函式），會根據目前的 State 與收到的 Action，回傳新的 State。",[1216,1549,1550],{},"先定義購物車的資料型別。",[1216,1552,1553],{},[1220,1554,1555],{},"cart.types.ts",[1308,1557,1559],{"className":1310,"code":1558,"language":1312,"meta":1313,"style":1313},"export interface Product {\n  id: number;\n  name: string;\n  price: number;\n}\n\nexport interface CartItem extends Product {\n  quantity: number;\n}\n\nexport interface CartState {\n  items: CartItem[];\n}\n",[1220,1560,1561,1573,1584,1596,1607,1612,1616,1633,1644,1648,1652,1663,1675],{"__ignoreMap":1313},[1317,1562,1563,1565,1568,1570],{"class":1319,"line":340},[1317,1564,1365],{"class":1322},[1317,1566,1567],{"class":1322}," interface",[1317,1569,1409],{"class":1408},[1317,1571,1572],{"class":1326}," {\n",[1317,1574,1575,1578,1580,1582],{"class":1319,"line":329},[1317,1576,1577],{"class":1401},"  id",[1317,1579,1405],{"class":1322},[1317,1581,1457],{"class":1371},[1317,1583,1337],{"class":1326},[1317,1585,1586,1589,1591,1594],{"class":1319,"line":318},[1317,1587,1588],{"class":1401},"  name",[1317,1590,1405],{"class":1322},[1317,1592,1593],{"class":1371}," string",[1317,1595,1337],{"class":1326},[1317,1597,1598,1601,1603,1605],{"class":1319,"line":134},[1317,1599,1600],{"class":1401},"  price",[1317,1602,1405],{"class":1322},[1317,1604,1457],{"class":1371},[1317,1606,1337],{"class":1326},[1317,1608,1609],{"class":1319,"line":122},[1317,1610,1611],{"class":1326},"}\n",[1317,1613,1614],{"class":1319,"line":31},[1317,1615,1360],{"emptyLinePlaceholder":1359},[1317,1617,1618,1620,1622,1625,1628,1631],{"class":1319,"line":17},[1317,1619,1365],{"class":1322},[1317,1621,1567],{"class":1322},[1317,1623,1624],{"class":1408}," CartItem",[1317,1626,1627],{"class":1322}," extends",[1317,1629,1409],{"class":1630},"sMOND",[1317,1632,1572],{"class":1326},[1317,1634,1635,1638,1640,1642],{"class":1319,"line":110},[1317,1636,1637],{"class":1401},"  quantity",[1317,1639,1405],{"class":1322},[1317,1641,1457],{"class":1371},[1317,1643,1337],{"class":1326},[1317,1645,1646],{"class":1319,"line":76},[1317,1647,1611],{"class":1326},[1317,1649,1650],{"class":1319,"line":247},[1317,1651,1360],{"emptyLinePlaceholder":1359},[1317,1653,1654,1656,1658,1661],{"class":1319,"line":236},[1317,1655,1365],{"class":1322},[1317,1657,1567],{"class":1322},[1317,1659,1660],{"class":1408}," CartState",[1317,1662,1572],{"class":1326},[1317,1664,1665,1668,1670,1672],{"class":1319,"line":225},[1317,1666,1667],{"class":1401},"  items",[1317,1669,1405],{"class":1322},[1317,1671,1624],{"class":1408},[1317,1673,1674],{"class":1326},"[];\n",[1317,1676,1677],{"class":1319,"line":214},[1317,1678,1611],{"class":1326},[1216,1680,1681,1682,1685],{},"接著使用 ",[1220,1683,1684],{},"createReducer()"," 建立購物車 Reducer。",[1216,1687,1688],{},[1220,1689,1690],{},"cart.reducer.ts",[1308,1692,1694],{"className":1310,"code":1693,"language":1312,"meta":1313,"style":1313},"import { createReducer, on } from '@ngrx\u002Fstore';\n\nimport {\n  addProduct,\n  removeProduct,\n  clearCart\n} from '.\u002Fcart.actions';\n\nimport type { CartState } from '.\u002Fcart.types';\n\nexport const initialState: CartState = {\n  items: []\n};\n\nexport const cartReducer = createReducer(\n  initialState,\n\n  on(addProduct, (state, { product }) => {\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 { ...state, items };\n  }),\n\n  on(removeProduct, (state, { id }) => ({\n    ...state,\n    items: state.items.filter(item => item.id !== id)\n  })),\n\n  on(clearCart, state => ({\n    ...state,\n    items: []\n  }))\n);\n",[1220,1695,1696,1709,1713,1719,1724,1729,1734,1746,1750,1765,1769,1786,1791,1796,1800,1816,1821,1825,1849,1867,1884,1889,1893,1905,1922,1931,1954,1962,1967,1991,1995,2008,2014,2019,2040,2049,2071,2077,2082,2096,2103,2109,2115],{"__ignoreMap":1313},[1317,1697,1698,1700,1703,1705,1707],{"class":1319,"line":340},[1317,1699,1323],{"class":1322},[1317,1701,1702],{"class":1326}," { createReducer, on } ",[1317,1704,1330],{"class":1322},[1317,1706,1334],{"class":1333},[1317,1708,1337],{"class":1326},[1317,1710,1711],{"class":1319,"line":329},[1317,1712,1360],{"emptyLinePlaceholder":1359},[1317,1714,1715,1717],{"class":1319,"line":318},[1317,1716,1323],{"class":1322},[1317,1718,1572],{"class":1326},[1317,1720,1721],{"class":1319,"line":134},[1317,1722,1723],{"class":1326},"  addProduct,\n",[1317,1725,1726],{"class":1319,"line":122},[1317,1727,1728],{"class":1326},"  removeProduct,\n",[1317,1730,1731],{"class":1319,"line":31},[1317,1732,1733],{"class":1326},"  clearCart\n",[1317,1735,1736,1739,1741,1744],{"class":1319,"line":17},[1317,1737,1738],{"class":1326},"} ",[1317,1740,1330],{"class":1322},[1317,1742,1743],{"class":1333}," '.\u002Fcart.actions'",[1317,1745,1337],{"class":1326},[1317,1747,1748],{"class":1319,"line":110},[1317,1749,1360],{"emptyLinePlaceholder":1359},[1317,1751,1752,1754,1756,1759,1761,1763],{"class":1319,"line":76},[1317,1753,1323],{"class":1322},[1317,1755,1344],{"class":1322},[1317,1757,1758],{"class":1326}," { CartState } ",[1317,1760,1330],{"class":1322},[1317,1762,1352],{"class":1333},[1317,1764,1337],{"class":1326},[1317,1766,1767],{"class":1319,"line":247},[1317,1768,1360],{"emptyLinePlaceholder":1359},[1317,1770,1771,1773,1775,1778,1780,1782,1784],{"class":1319,"line":236},[1317,1772,1365],{"class":1322},[1317,1774,1368],{"class":1322},[1317,1776,1777],{"class":1371}," initialState",[1317,1779,1405],{"class":1322},[1317,1781,1660],{"class":1408},[1317,1783,1375],{"class":1322},[1317,1785,1572],{"class":1326},[1317,1787,1788],{"class":1319,"line":225},[1317,1789,1790],{"class":1326},"  items: []\n",[1317,1792,1793],{"class":1319,"line":214},[1317,1794,1795],{"class":1326},"};\n",[1317,1797,1798],{"class":1319,"line":203},[1317,1799,1360],{"emptyLinePlaceholder":1359},[1317,1801,1802,1804,1806,1809,1811,1814],{"class":1319,"line":192},[1317,1803,1365],{"class":1322},[1317,1805,1368],{"class":1322},[1317,1807,1808],{"class":1371}," cartReducer",[1317,1810,1375],{"class":1322},[1317,1812,1813],{"class":1378}," createReducer",[1317,1815,1382],{"class":1326},[1317,1817,1818],{"class":1319,"line":181},[1317,1819,1820],{"class":1326},"  initialState,\n",[1317,1822,1823],{"class":1319,"line":170},[1317,1824,1360],{"emptyLinePlaceholder":1359},[1317,1826,1827,1830,1833,1836,1839,1841,1844,1847],{"class":1319,"line":159},[1317,1828,1829],{"class":1378},"  on",[1317,1831,1832],{"class":1326},"(addProduct, (",[1317,1834,1835],{"class":1401},"state",[1317,1837,1838],{"class":1326},", { ",[1317,1840,1402],{"class":1401},[1317,1842,1843],{"class":1326}," }) ",[1317,1845,1846],{"class":1322},"=>",[1317,1848,1572],{"class":1326},[1317,1850,1851,1854,1857,1859,1862,1865],{"class":1319,"line":148},[1317,1852,1853],{"class":1322},"    const",[1317,1855,1856],{"class":1371}," existing",[1317,1858,1375],{"class":1322},[1317,1860,1861],{"class":1326}," state.items.",[1317,1863,1864],{"class":1378},"find",[1317,1866,1382],{"class":1326},[1317,1868,1869,1872,1875,1878,1881],{"class":1319,"line":137},[1317,1870,1871],{"class":1401},"      item",[1317,1873,1874],{"class":1322}," =>",[1317,1876,1877],{"class":1326}," item.id ",[1317,1879,1880],{"class":1322},"===",[1317,1882,1883],{"class":1326}," product.id\n",[1317,1885,1886],{"class":1319,"line":125},[1317,1887,1888],{"class":1326},"    );\n",[1317,1890,1891],{"class":1319,"line":113},[1317,1892,1360],{"emptyLinePlaceholder":1359},[1317,1894,1895,1897,1900,1902],{"class":1319,"line":101},[1317,1896,1853],{"class":1322},[1317,1898,1899],{"class":1371}," items",[1317,1901,1375],{"class":1322},[1317,1903,1904],{"class":1326}," existing\n",[1317,1906,1907,1910,1912,1914,1916,1919],{"class":1319,"line":90},[1317,1908,1909],{"class":1322},"      ?",[1317,1911,1861],{"class":1326},[1317,1913,1043],{"class":1378},[1317,1915,1528],{"class":1326},[1317,1917,1918],{"class":1401},"item",[1317,1920,1921],{"class":1322}," =>\n",[1317,1923,1924,1927,1929],{"class":1319,"line":79},[1317,1925,1926],{"class":1326},"          item.id ",[1317,1928,1880],{"class":1322},[1317,1930,1883],{"class":1326},[1317,1932,1933,1936,1939,1942,1945,1948,1951],{"class":1319,"line":67},[1317,1934,1935],{"class":1322},"            ?",[1317,1937,1938],{"class":1326}," { ",[1317,1940,1941],{"class":1322},"...",[1317,1943,1944],{"class":1326},"item, quantity: item.quantity ",[1317,1946,1947],{"class":1322},"+",[1317,1949,1950],{"class":1371}," 1",[1317,1952,1953],{"class":1326}," }\n",[1317,1955,1956,1959],{"class":1319,"line":56},[1317,1957,1958],{"class":1322},"            :",[1317,1960,1961],{"class":1326}," item\n",[1317,1963,1964],{"class":1319,"line":45},[1317,1965,1966],{"class":1326},"        )\n",[1317,1968,1969,1972,1975,1977,1980,1982,1985,1988],{"class":1319,"line":34},[1317,1970,1971],{"class":1322},"      :",[1317,1973,1974],{"class":1326}," [",[1317,1976,1941],{"class":1322},[1317,1978,1979],{"class":1326},"state.items, { ",[1317,1981,1941],{"class":1322},[1317,1983,1984],{"class":1326},"product, quantity: ",[1317,1986,1987],{"class":1371},"1",[1317,1989,1990],{"class":1326}," }];\n",[1317,1992,1993],{"class":1319,"line":22},[1317,1994,1360],{"emptyLinePlaceholder":1359},[1317,1996,1998,2001,2003,2005],{"class":1319,"line":1997},31,[1317,1999,2000],{"class":1322},"    return",[1317,2002,1938],{"class":1326},[1317,2004,1941],{"class":1322},[1317,2006,2007],{"class":1326},"state, items };\n",[1317,2009,2011],{"class":1319,"line":2010},32,[1317,2012,2013],{"class":1326},"  }),\n",[1317,2015,2017],{"class":1319,"line":2016},33,[1317,2018,1360],{"emptyLinePlaceholder":1359},[1317,2020,2022,2024,2027,2029,2031,2033,2035,2037],{"class":1319,"line":2021},34,[1317,2023,1829],{"class":1378},[1317,2025,2026],{"class":1326},"(removeProduct, (",[1317,2028,1835],{"class":1401},[1317,2030,1838],{"class":1326},[1317,2032,1452],{"class":1401},[1317,2034,1843],{"class":1326},[1317,2036,1846],{"class":1322},[1317,2038,2039],{"class":1326}," ({\n",[1317,2041,2043,2046],{"class":1319,"line":2042},35,[1317,2044,2045],{"class":1322},"    ...",[1317,2047,2048],{"class":1326},"state,\n",[1317,2050,2052,2055,2057,2059,2061,2063,2065,2068],{"class":1319,"line":2051},36,[1317,2053,2054],{"class":1326},"    items: state.items.",[1317,2056,1044],{"class":1378},[1317,2058,1528],{"class":1326},[1317,2060,1918],{"class":1401},[1317,2062,1874],{"class":1322},[1317,2064,1877],{"class":1326},[1317,2066,2067],{"class":1322},"!==",[1317,2069,2070],{"class":1326}," id)\n",[1317,2072,2074],{"class":1319,"line":2073},37,[1317,2075,2076],{"class":1326},"  })),\n",[1317,2078,2080],{"class":1319,"line":2079},38,[1317,2081,1360],{"emptyLinePlaceholder":1359},[1317,2083,2085,2087,2090,2092,2094],{"class":1319,"line":2084},39,[1317,2086,1829],{"class":1378},[1317,2088,2089],{"class":1326},"(clearCart, ",[1317,2091,1835],{"class":1401},[1317,2093,1874],{"class":1322},[1317,2095,2039],{"class":1326},[1317,2097,2099,2101],{"class":1319,"line":2098},40,[1317,2100,2045],{"class":1322},[1317,2102,2048],{"class":1326},[1317,2104,2106],{"class":1319,"line":2105},41,[1317,2107,2108],{"class":1326},"    items: []\n",[1317,2110,2112],{"class":1319,"line":2111},42,[1317,2113,2114],{"class":1326},"  }))\n",[1317,2116,2118],{"class":1319,"line":2117},43,[1317,2119,1417],{"class":1326},[1216,2121,2122,2124,2125,2128],{},[1220,2123,1684],{}," 用來建立 Reducer，",[1220,2126,2127],{},"on()"," 則指定不同 Action 對應的更新邏輯。",[1216,2130,2131,2132,2134],{},"以 ",[1220,2133,1500],{}," 為例，會先檢查商品是否已經存在。如果存在就增加數量，否則加入新商品。",[1216,2136,2137,2138,2141],{},"這裡要注意，",[1240,2139,2140],{},"Reducer 必須遵守不可變性（Immutability）","，不能直接修改原本的 State，而是要建立並回傳新的資料。",[1216,2143,2144,2145,2148],{},"例如使用 ",[1220,2146,2147],{},"state.items.push()"," 會直接修改原本的陣列，可能讓依賴參考比較的 Selector 無法正確重新計算。在 NgRx 預設啟用的開發環境 Runtime Checks 下，也可能因此拋出錯誤。",[1216,2150,2151,2152,1231,2155,2158],{},"所以範例使用展開運算子、",[1220,2153,2154],{},"map()",[1220,2156,2157],{},"filter()"," 建立新資料。Reducer 也必須保持純函式的特性，不應在裡面呼叫 API、操作 DOM 或執行其他副作用。",[1291,2160,2162],{"id":2161},"使用-selector-取得資料","使用 Selector 取得資料",[1216,2164,2165],{},"除了更新 State，購物車還需要計算商品數量與總金額。",[1216,2167,2168,2169,1231,2172,2175],{},"NgRx 提供 ",[1220,2170,2171],{},"createFeatureSelector()",[1220,2173,2174],{},"createSelector()","，讓我們取得指定的 State，並計算衍生資料。",[1216,2177,2178],{},[1220,2179,2180],{},"cart.selectors.ts",[1308,2182,2184],{"className":1310,"code":2183,"language":1312,"meta":1313,"style":1313},"import {\n  createFeatureSelector,\n  createSelector\n} from '@ngrx\u002Fstore';\n\nimport type { CartState } from '.\u002Fcart.types';\n\nexport const selectCartState =\n  createFeatureSelector\u003CCartState>('cart');\n\nexport const selectCartItems = createSelector(\n  selectCartState,\n  state => state.items\n);\n\nexport const selectCartCount = createSelector(\n  selectCartItems,\n  items => items.reduce(\n    (sum, item) => sum + item.quantity,\n    0\n  )\n);\n\nexport const selectCartTotal = createSelector(\n  selectCartItems,\n  items => items.reduce(\n    (sum, item) => sum + item.price * item.quantity,\n    0\n  )\n);\n",[1220,2185,2186,2192,2197,2202,2212,2216,2230,2234,2246,2265,2269,2285,2290,2300,2304,2308,2323,2328,2341,2367,2372,2377,2381,2385,2400,2404,2416,2442,2446,2450],{"__ignoreMap":1313},[1317,2187,2188,2190],{"class":1319,"line":340},[1317,2189,1323],{"class":1322},[1317,2191,1572],{"class":1326},[1317,2193,2194],{"class":1319,"line":329},[1317,2195,2196],{"class":1326},"  createFeatureSelector,\n",[1317,2198,2199],{"class":1319,"line":318},[1317,2200,2201],{"class":1326},"  createSelector\n",[1317,2203,2204,2206,2208,2210],{"class":1319,"line":134},[1317,2205,1738],{"class":1326},[1317,2207,1330],{"class":1322},[1317,2209,1334],{"class":1333},[1317,2211,1337],{"class":1326},[1317,2213,2214],{"class":1319,"line":122},[1317,2215,1360],{"emptyLinePlaceholder":1359},[1317,2217,2218,2220,2222,2224,2226,2228],{"class":1319,"line":31},[1317,2219,1323],{"class":1322},[1317,2221,1344],{"class":1322},[1317,2223,1758],{"class":1326},[1317,2225,1330],{"class":1322},[1317,2227,1352],{"class":1333},[1317,2229,1337],{"class":1326},[1317,2231,2232],{"class":1319,"line":17},[1317,2233,1360],{"emptyLinePlaceholder":1359},[1317,2235,2236,2238,2240,2243],{"class":1319,"line":110},[1317,2237,1365],{"class":1322},[1317,2239,1368],{"class":1322},[1317,2241,2242],{"class":1371}," selectCartState",[1317,2244,2245],{"class":1322}," =\n",[1317,2247,2248,2251,2254,2257,2260,2263],{"class":1319,"line":76},[1317,2249,2250],{"class":1378},"  createFeatureSelector",[1317,2252,2253],{"class":1326},"\u003C",[1317,2255,2256],{"class":1408},"CartState",[1317,2258,2259],{"class":1326},">(",[1317,2261,2262],{"class":1333},"'cart'",[1317,2264,1417],{"class":1326},[1317,2266,2267],{"class":1319,"line":247},[1317,2268,1360],{"emptyLinePlaceholder":1359},[1317,2270,2271,2273,2275,2278,2280,2283],{"class":1319,"line":236},[1317,2272,1365],{"class":1322},[1317,2274,1368],{"class":1322},[1317,2276,2277],{"class":1371}," selectCartItems",[1317,2279,1375],{"class":1322},[1317,2281,2282],{"class":1378}," createSelector",[1317,2284,1382],{"class":1326},[1317,2286,2287],{"class":1319,"line":225},[1317,2288,2289],{"class":1326},"  selectCartState,\n",[1317,2291,2292,2295,2297],{"class":1319,"line":214},[1317,2293,2294],{"class":1401},"  state",[1317,2296,1874],{"class":1322},[1317,2298,2299],{"class":1326}," state.items\n",[1317,2301,2302],{"class":1319,"line":203},[1317,2303,1417],{"class":1326},[1317,2305,2306],{"class":1319,"line":192},[1317,2307,1360],{"emptyLinePlaceholder":1359},[1317,2309,2310,2312,2314,2317,2319,2321],{"class":1319,"line":181},[1317,2311,1365],{"class":1322},[1317,2313,1368],{"class":1322},[1317,2315,2316],{"class":1371}," selectCartCount",[1317,2318,1375],{"class":1322},[1317,2320,2282],{"class":1378},[1317,2322,1382],{"class":1326},[1317,2324,2325],{"class":1319,"line":170},[1317,2326,2327],{"class":1326},"  selectCartItems,\n",[1317,2329,2330,2332,2334,2337,2339],{"class":1319,"line":159},[1317,2331,1667],{"class":1401},[1317,2333,1874],{"class":1322},[1317,2335,2336],{"class":1326}," items.",[1317,2338,1045],{"class":1378},[1317,2340,1382],{"class":1326},[1317,2342,2343,2346,2349,2352,2354,2357,2359,2362,2364],{"class":1319,"line":148},[1317,2344,2345],{"class":1326},"    (",[1317,2347,2348],{"class":1401},"sum",[1317,2350,2351],{"class":1326},", ",[1317,2353,1918],{"class":1401},[1317,2355,2356],{"class":1326},") ",[1317,2358,1846],{"class":1322},[1317,2360,2361],{"class":1326}," sum ",[1317,2363,1947],{"class":1322},[1317,2365,2366],{"class":1326}," item.quantity,\n",[1317,2368,2369],{"class":1319,"line":137},[1317,2370,2371],{"class":1371},"    0\n",[1317,2373,2374],{"class":1319,"line":125},[1317,2375,2376],{"class":1326},"  )\n",[1317,2378,2379],{"class":1319,"line":113},[1317,2380,1417],{"class":1326},[1317,2382,2383],{"class":1319,"line":101},[1317,2384,1360],{"emptyLinePlaceholder":1359},[1317,2386,2387,2389,2391,2394,2396,2398],{"class":1319,"line":90},[1317,2388,1365],{"class":1322},[1317,2390,1368],{"class":1322},[1317,2392,2393],{"class":1371}," selectCartTotal",[1317,2395,1375],{"class":1322},[1317,2397,2282],{"class":1378},[1317,2399,1382],{"class":1326},[1317,2401,2402],{"class":1319,"line":79},[1317,2403,2327],{"class":1326},[1317,2405,2406,2408,2410,2412,2414],{"class":1319,"line":67},[1317,2407,1667],{"class":1401},[1317,2409,1874],{"class":1322},[1317,2411,2336],{"class":1326},[1317,2413,1045],{"class":1378},[1317,2415,1382],{"class":1326},[1317,2417,2418,2420,2422,2424,2426,2428,2430,2432,2434,2437,2440],{"class":1319,"line":56},[1317,2419,2345],{"class":1326},[1317,2421,2348],{"class":1401},[1317,2423,2351],{"class":1326},[1317,2425,1918],{"class":1401},[1317,2427,2356],{"class":1326},[1317,2429,1846],{"class":1322},[1317,2431,2361],{"class":1326},[1317,2433,1947],{"class":1322},[1317,2435,2436],{"class":1326}," item.price ",[1317,2438,2439],{"class":1322},"*",[1317,2441,2366],{"class":1326},[1317,2443,2444],{"class":1319,"line":45},[1317,2445,2371],{"class":1371},[1317,2447,2448],{"class":1319,"line":34},[1317,2449,2376],{"class":1326},[1317,2451,2452],{"class":1319,"line":22},[1317,2453,1417],{"class":1326},[1216,2455,2456,2459,2460,2463,2464,2466],{},[1220,2457,2458],{},"createFeatureSelector('cart')"," 用來取得 Store 中 ",[1220,2461,2462],{},"cart"," 這個 Key 對應的 State，而 ",[1220,2465,2174],{}," 可以根據其他 Selector 的結果取得或計算資料。",[1216,2468,2469],{},"Selector 也具備 Memoization（記憶化）機制。當輸入沒有改變時，可以重複使用之前的計算結果，避免不必要的運算。",[1251,2471,2473],{"id":2472},"在-angular-使用-ngrx-store","在 Angular 使用 NgRx Store",[1216,2475,2476],{},"Action、Reducer 與 Selector 準備完成後，就可以將 Store 註冊到 Angular Application。",[1291,2478,2480],{"id":2479},"註冊-store","註冊 Store",[1216,2482,2483],{},[1220,2484,2485],{},"app.config.ts",[1308,2487,2489],{"className":1310,"code":2488,"language":1312,"meta":1313,"style":1313},"import { ApplicationConfig } from '@angular\u002Fcore';\nimport { provideStore } from '@ngrx\u002Fstore';\n\nimport { cartReducer } from '.\u002Fcart\u002Fcart.reducer';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideStore({\n      cart: cartReducer\n    })\n  ]\n};\n",[1220,2490,2491,2505,2518,2522,2536,2540,2558,2563,2571,2576,2581,2586],{"__ignoreMap":1313},[1317,2492,2493,2495,2498,2500,2503],{"class":1319,"line":340},[1317,2494,1323],{"class":1322},[1317,2496,2497],{"class":1326}," { ApplicationConfig } ",[1317,2499,1330],{"class":1322},[1317,2501,2502],{"class":1333}," '@angular\u002Fcore'",[1317,2504,1337],{"class":1326},[1317,2506,2507,2509,2512,2514,2516],{"class":1319,"line":329},[1317,2508,1323],{"class":1322},[1317,2510,2511],{"class":1326}," { provideStore } ",[1317,2513,1330],{"class":1322},[1317,2515,1334],{"class":1333},[1317,2517,1337],{"class":1326},[1317,2519,2520],{"class":1319,"line":318},[1317,2521,1360],{"emptyLinePlaceholder":1359},[1317,2523,2524,2526,2529,2531,2534],{"class":1319,"line":134},[1317,2525,1323],{"class":1322},[1317,2527,2528],{"class":1326}," { cartReducer } ",[1317,2530,1330],{"class":1322},[1317,2532,2533],{"class":1333}," '.\u002Fcart\u002Fcart.reducer'",[1317,2535,1337],{"class":1326},[1317,2537,2538],{"class":1319,"line":122},[1317,2539,1360],{"emptyLinePlaceholder":1359},[1317,2541,2542,2544,2546,2549,2551,2554,2556],{"class":1319,"line":31},[1317,2543,1365],{"class":1322},[1317,2545,1368],{"class":1322},[1317,2547,2548],{"class":1371}," appConfig",[1317,2550,1405],{"class":1322},[1317,2552,2553],{"class":1408}," ApplicationConfig",[1317,2555,1375],{"class":1322},[1317,2557,1572],{"class":1326},[1317,2559,2560],{"class":1319,"line":17},[1317,2561,2562],{"class":1326},"  providers: [\n",[1317,2564,2565,2568],{"class":1319,"line":110},[1317,2566,2567],{"class":1378},"    provideStore",[1317,2569,2570],{"class":1326},"({\n",[1317,2572,2573],{"class":1319,"line":76},[1317,2574,2575],{"class":1326},"      cart: cartReducer\n",[1317,2577,2578],{"class":1319,"line":247},[1317,2579,2580],{"class":1326},"    })\n",[1317,2582,2583],{"class":1319,"line":236},[1317,2584,2585],{"class":1326},"  ]\n",[1317,2587,2588],{"class":1319,"line":225},[1317,2589,1795],{"class":1326},[1216,2591,2592,2593,2596,2597,2599,2600,2602],{},"這裡透過 ",[1220,2594,2595],{},"provideStore()"," 註冊購物車 Reducer，並使用 ",[1220,2598,2462],{}," 作為 State 的 Key，與前面 ",[1220,2601,2458],{}," 對應。",[1291,2604,2606],{"id":2605},"component-使用-store","Component 使用 Store",[1216,2608,2609,2610,1243],{},"完成註冊後，就能透過 Angular DI 注入 ",[1220,2611,2612],{},"Store",[1216,2614,2615,2616,2619,2620,2623],{},"傳統 NgRx Store 可以使用 ",[1220,2617,2618],{},"select()"," 取得 Observable，也能透過 ",[1220,2621,2622],{},"selectSignal()"," 取得 Signal。例如讀取購物車總金額：",[1308,2625,2627],{"className":1310,"code":2626,"language":1312,"meta":1313,"style":1313},"\u002F\u002F Observable\nreadonly total$ = this.store.select(selectCartTotal);\n\n\u002F\u002F Signal\nreadonly total = this.store.selectSignal(selectCartTotal);\n",[1220,2628,2629,2635,2655,2659,2664],{"__ignoreMap":1313},[1317,2630,2631],{"class":1319,"line":340},[1317,2632,2634],{"class":2633},"sDN9O","\u002F\u002F Observable\n",[1317,2636,2637,2640,2643,2646,2649,2652],{"class":1319,"line":329},[1317,2638,2639],{"class":1326},"readonly total$ ",[1317,2641,2642],{"class":1322},"=",[1317,2644,2645],{"class":1371}," this",[1317,2647,2648],{"class":1326},".store.",[1317,2650,2651],{"class":1378},"select",[1317,2653,2654],{"class":1326},"(selectCartTotal);\n",[1317,2656,2657],{"class":1319,"line":318},[1317,2658,1360],{"emptyLinePlaceholder":1359},[1317,2660,2661],{"class":1319,"line":134},[1317,2662,2663],{"class":2633},"\u002F\u002F Signal\n",[1317,2665,2666,2669,2671,2673,2675,2678],{"class":1319,"line":122},[1317,2667,2668],{"class":1326},"readonly total ",[1317,2670,2642],{"class":1322},[1317,2672,2645],{"class":1371},[1317,2674,2648],{"class":1326},[1317,2676,2677],{"class":1378},"selectSignal",[1317,2679,2654],{"class":1326},[1216,2681,2682,2683,2685,2686,2688,2689,2692,2693,2695,2696,1243],{},"如果需要搭配 RxJS 的 ",[1220,2684,2154],{},"、",[1220,2687,2157],{}," 或 ",[1220,2690,2691],{},"combineLatest()"," 處理資料，可以使用 ",[1220,2694,2618],{},"；如果希望在 Template 直接以 Signal 讀取資料，則可以使用 ",[1220,2697,2622],{},[1216,2699,2700,2701,1243],{},"這次為了與之前的 SignalStore 比較，我們使用 ",[1220,2702,2622],{},[1216,2704,2705],{},[1220,2706,2707],{},"cart.ts",[1308,2709,2711],{"className":1310,"code":2710,"language":1312,"meta":1313,"style":1313},"import { Component, inject } from '@angular\u002Fcore';\nimport { Store } from '@ngrx\u002Fstore';\n\nimport {\n  addProduct,\n  removeProduct,\n  clearCart\n} from '.\u002Fcart.actions';\n\nimport {\n  selectCartItems,\n  selectCartCount,\n  selectCartTotal\n} from '.\u002Fcart.selectors';\n\nimport type { Product } from '.\u002Fcart.types';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003Cmain>\n      \u003Ch2>商品列表\u003C\u002Fh2>\n\n      \u003Cbutton (click)=\"add({\n        id: 1,\n        name: '鍵盤',\n        price: 1200\n      })\">\n        加入鍵盤\n      \u003C\u002Fbutton>\n\n      \u003Cbutton (click)=\"add({\n        id: 2,\n        name: '滑鼠',\n        price: 800\n      })\">\n        加入滑鼠\n      \u003C\u002Fbutton>\n\n      \u003Chr \u002F>\n\n      \u003Ch2>購物車\u003C\u002Fh2>\n\n      @for (item of items(); track item.id) {\n        \u003Cp>\n          {{ item.name }} × {{ item.quantity }}\n\n          \u003Cbutton (click)=\"remove(item.id)\">\n            移除\n          \u003C\u002Fbutton>\n        \u003C\u002Fp>\n      } @empty {\n        \u003Cp>購物車目前沒有商品\u003C\u002Fp>\n      }\n\n      \u003Cp>商品數量：{{ count() }}\u003C\u002Fp>\n      \u003Cp>總金額：{{ total() }} 元\u003C\u002Fp>\n\n      \u003Cbutton (click)=\"clear()\">\n        清空購物車\n      \u003C\u002Fbutton>\n    \u003C\u002Fmain>\n  `\n})\nexport class CartComponent {\n  private readonly store = inject(Store);\n\n  readonly items = this.store.selectSignal(selectCartItems);\n  readonly count = this.store.selectSignal(selectCartCount);\n  readonly total = this.store.selectSignal(selectCartTotal);\n\n  add(product: Product) {\n    this.store.dispatch(addProduct({ product }));\n  }\n\n  remove(id: number) {\n    this.store.dispatch(removeProduct({ id }));\n  }\n\n  clear() {\n    this.store.dispatch(clearCart());\n  }\n}\n",[1220,2712,2713,2726,2739,2743,2749,2753,2757,2761,2771,2775,2781,2785,2790,2795,2806,2810,2824,2828,2838,2848,2856,2861,2866,2870,2875,2880,2885,2890,2895,2900,2905,2909,2913,2918,2923,2928,2932,2937,2941,2945,2950,2954,2959,2963,2969,2975,2981,2986,2992,2998,3004,3010,3016,3022,3028,3033,3039,3045,3050,3056,3062,3067,3073,3079,3085,3098,3118,3123,3142,3161,3179,3184,3201,3217,3223,3228,3244,3261,3266,3271,3280,3296,3301],{"__ignoreMap":1313},[1317,2714,2715,2717,2720,2722,2724],{"class":1319,"line":340},[1317,2716,1323],{"class":1322},[1317,2718,2719],{"class":1326}," { Component, inject } ",[1317,2721,1330],{"class":1322},[1317,2723,2502],{"class":1333},[1317,2725,1337],{"class":1326},[1317,2727,2728,2730,2733,2735,2737],{"class":1319,"line":329},[1317,2729,1323],{"class":1322},[1317,2731,2732],{"class":1326}," { Store } ",[1317,2734,1330],{"class":1322},[1317,2736,1334],{"class":1333},[1317,2738,1337],{"class":1326},[1317,2740,2741],{"class":1319,"line":318},[1317,2742,1360],{"emptyLinePlaceholder":1359},[1317,2744,2745,2747],{"class":1319,"line":134},[1317,2746,1323],{"class":1322},[1317,2748,1572],{"class":1326},[1317,2750,2751],{"class":1319,"line":122},[1317,2752,1723],{"class":1326},[1317,2754,2755],{"class":1319,"line":31},[1317,2756,1728],{"class":1326},[1317,2758,2759],{"class":1319,"line":17},[1317,2760,1733],{"class":1326},[1317,2762,2763,2765,2767,2769],{"class":1319,"line":110},[1317,2764,1738],{"class":1326},[1317,2766,1330],{"class":1322},[1317,2768,1743],{"class":1333},[1317,2770,1337],{"class":1326},[1317,2772,2773],{"class":1319,"line":76},[1317,2774,1360],{"emptyLinePlaceholder":1359},[1317,2776,2777,2779],{"class":1319,"line":247},[1317,2778,1323],{"class":1322},[1317,2780,1572],{"class":1326},[1317,2782,2783],{"class":1319,"line":236},[1317,2784,2327],{"class":1326},[1317,2786,2787],{"class":1319,"line":225},[1317,2788,2789],{"class":1326},"  selectCartCount,\n",[1317,2791,2792],{"class":1319,"line":214},[1317,2793,2794],{"class":1326},"  selectCartTotal\n",[1317,2796,2797,2799,2801,2804],{"class":1319,"line":203},[1317,2798,1738],{"class":1326},[1317,2800,1330],{"class":1322},[1317,2802,2803],{"class":1333}," '.\u002Fcart.selectors'",[1317,2805,1337],{"class":1326},[1317,2807,2808],{"class":1319,"line":192},[1317,2809,1360],{"emptyLinePlaceholder":1359},[1317,2811,2812,2814,2816,2818,2820,2822],{"class":1319,"line":181},[1317,2813,1323],{"class":1322},[1317,2815,1344],{"class":1322},[1317,2817,1347],{"class":1326},[1317,2819,1330],{"class":1322},[1317,2821,1352],{"class":1333},[1317,2823,1337],{"class":1326},[1317,2825,2826],{"class":1319,"line":170},[1317,2827,1360],{"emptyLinePlaceholder":1359},[1317,2829,2830,2833,2836],{"class":1319,"line":159},[1317,2831,2832],{"class":1326},"@",[1317,2834,2835],{"class":1378},"Component",[1317,2837,2570],{"class":1326},[1317,2839,2840,2843,2846],{"class":1319,"line":148},[1317,2841,2842],{"class":1326},"  selector: ",[1317,2844,2845],{"class":1333},"'app-root'",[1317,2847,1390],{"class":1326},[1317,2849,2850,2853],{"class":1319,"line":137},[1317,2851,2852],{"class":1326},"  template: ",[1317,2854,2855],{"class":1333},"`\n",[1317,2857,2858],{"class":1319,"line":125},[1317,2859,2860],{"class":1333},"    \u003Cmain>\n",[1317,2862,2863],{"class":1319,"line":113},[1317,2864,2865],{"class":1333},"      \u003Ch2>商品列表\u003C\u002Fh2>\n",[1317,2867,2868],{"class":1319,"line":101},[1317,2869,1360],{"emptyLinePlaceholder":1359},[1317,2871,2872],{"class":1319,"line":90},[1317,2873,2874],{"class":1333},"      \u003Cbutton (click)=\"add({\n",[1317,2876,2877],{"class":1319,"line":79},[1317,2878,2879],{"class":1333},"        id: 1,\n",[1317,2881,2882],{"class":1319,"line":67},[1317,2883,2884],{"class":1333},"        name: '鍵盤',\n",[1317,2886,2887],{"class":1319,"line":56},[1317,2888,2889],{"class":1333},"        price: 1200\n",[1317,2891,2892],{"class":1319,"line":45},[1317,2893,2894],{"class":1333},"      })\">\n",[1317,2896,2897],{"class":1319,"line":34},[1317,2898,2899],{"class":1333},"        加入鍵盤\n",[1317,2901,2902],{"class":1319,"line":22},[1317,2903,2904],{"class":1333},"      \u003C\u002Fbutton>\n",[1317,2906,2907],{"class":1319,"line":1997},[1317,2908,1360],{"emptyLinePlaceholder":1359},[1317,2910,2911],{"class":1319,"line":2010},[1317,2912,2874],{"class":1333},[1317,2914,2915],{"class":1319,"line":2016},[1317,2916,2917],{"class":1333},"        id: 2,\n",[1317,2919,2920],{"class":1319,"line":2021},[1317,2921,2922],{"class":1333},"        name: '滑鼠',\n",[1317,2924,2925],{"class":1319,"line":2042},[1317,2926,2927],{"class":1333},"        price: 800\n",[1317,2929,2930],{"class":1319,"line":2051},[1317,2931,2894],{"class":1333},[1317,2933,2934],{"class":1319,"line":2073},[1317,2935,2936],{"class":1333},"        加入滑鼠\n",[1317,2938,2939],{"class":1319,"line":2079},[1317,2940,2904],{"class":1333},[1317,2942,2943],{"class":1319,"line":2084},[1317,2944,1360],{"emptyLinePlaceholder":1359},[1317,2946,2947],{"class":1319,"line":2098},[1317,2948,2949],{"class":1333},"      \u003Chr \u002F>\n",[1317,2951,2952],{"class":1319,"line":2105},[1317,2953,1360],{"emptyLinePlaceholder":1359},[1317,2955,2956],{"class":1319,"line":2111},[1317,2957,2958],{"class":1333},"      \u003Ch2>購物車\u003C\u002Fh2>\n",[1317,2960,2961],{"class":1319,"line":2117},[1317,2962,1360],{"emptyLinePlaceholder":1359},[1317,2964,2966],{"class":1319,"line":2965},44,[1317,2967,2968],{"class":1333},"      @for (item of items(); track item.id) {\n",[1317,2970,2972],{"class":1319,"line":2971},45,[1317,2973,2974],{"class":1333},"        \u003Cp>\n",[1317,2976,2978],{"class":1319,"line":2977},46,[1317,2979,2980],{"class":1333},"          {{ item.name }} × {{ item.quantity }}\n",[1317,2982,2984],{"class":1319,"line":2983},47,[1317,2985,1360],{"emptyLinePlaceholder":1359},[1317,2987,2989],{"class":1319,"line":2988},48,[1317,2990,2991],{"class":1333},"          \u003Cbutton (click)=\"remove(item.id)\">\n",[1317,2993,2995],{"class":1319,"line":2994},49,[1317,2996,2997],{"class":1333},"            移除\n",[1317,2999,3001],{"class":1319,"line":3000},50,[1317,3002,3003],{"class":1333},"          \u003C\u002Fbutton>\n",[1317,3005,3007],{"class":1319,"line":3006},51,[1317,3008,3009],{"class":1333},"        \u003C\u002Fp>\n",[1317,3011,3013],{"class":1319,"line":3012},52,[1317,3014,3015],{"class":1333},"      } @empty {\n",[1317,3017,3019],{"class":1319,"line":3018},53,[1317,3020,3021],{"class":1333},"        \u003Cp>購物車目前沒有商品\u003C\u002Fp>\n",[1317,3023,3025],{"class":1319,"line":3024},54,[1317,3026,3027],{"class":1333},"      }\n",[1317,3029,3031],{"class":1319,"line":3030},55,[1317,3032,1360],{"emptyLinePlaceholder":1359},[1317,3034,3036],{"class":1319,"line":3035},56,[1317,3037,3038],{"class":1333},"      \u003Cp>商品數量：{{ count() }}\u003C\u002Fp>\n",[1317,3040,3042],{"class":1319,"line":3041},57,[1317,3043,3044],{"class":1333},"      \u003Cp>總金額：{{ total() }} 元\u003C\u002Fp>\n",[1317,3046,3048],{"class":1319,"line":3047},58,[1317,3049,1360],{"emptyLinePlaceholder":1359},[1317,3051,3053],{"class":1319,"line":3052},59,[1317,3054,3055],{"class":1333},"      \u003Cbutton (click)=\"clear()\">\n",[1317,3057,3059],{"class":1319,"line":3058},60,[1317,3060,3061],{"class":1333},"        清空購物車\n",[1317,3063,3065],{"class":1319,"line":3064},61,[1317,3066,2904],{"class":1333},[1317,3068,3070],{"class":1319,"line":3069},62,[1317,3071,3072],{"class":1333},"    \u003C\u002Fmain>\n",[1317,3074,3076],{"class":1319,"line":3075},63,[1317,3077,3078],{"class":1333},"  `\n",[1317,3080,3082],{"class":1319,"line":3081},64,[1317,3083,3084],{"class":1326},"})\n",[1317,3086,3088,3090,3093,3096],{"class":1319,"line":3087},65,[1317,3089,1365],{"class":1322},[1317,3091,3092],{"class":1322}," class",[1317,3094,3095],{"class":1408}," CartComponent",[1317,3097,1572],{"class":1326},[1317,3099,3101,3104,3107,3110,3112,3115],{"class":1319,"line":3100},66,[1317,3102,3103],{"class":1322},"  private",[1317,3105,3106],{"class":1322}," readonly",[1317,3108,3109],{"class":1401}," store",[1317,3111,1375],{"class":1322},[1317,3113,3114],{"class":1378}," inject",[1317,3116,3117],{"class":1326},"(Store);\n",[1317,3119,3121],{"class":1319,"line":3120},67,[1317,3122,1360],{"emptyLinePlaceholder":1359},[1317,3124,3126,3129,3131,3133,3135,3137,3139],{"class":1319,"line":3125},68,[1317,3127,3128],{"class":1322},"  readonly",[1317,3130,1899],{"class":1401},[1317,3132,1375],{"class":1322},[1317,3134,2645],{"class":1371},[1317,3136,2648],{"class":1326},[1317,3138,2677],{"class":1378},[1317,3140,3141],{"class":1326},"(selectCartItems);\n",[1317,3143,3145,3147,3150,3152,3154,3156,3158],{"class":1319,"line":3144},69,[1317,3146,3128],{"class":1322},[1317,3148,3149],{"class":1401}," count",[1317,3151,1375],{"class":1322},[1317,3153,2645],{"class":1371},[1317,3155,2648],{"class":1326},[1317,3157,2677],{"class":1378},[1317,3159,3160],{"class":1326},"(selectCartCount);\n",[1317,3162,3164,3166,3169,3171,3173,3175,3177],{"class":1319,"line":3163},70,[1317,3165,3128],{"class":1322},[1317,3167,3168],{"class":1401}," total",[1317,3170,1375],{"class":1322},[1317,3172,2645],{"class":1371},[1317,3174,2648],{"class":1326},[1317,3176,2677],{"class":1378},[1317,3178,2654],{"class":1326},[1317,3180,3182],{"class":1319,"line":3181},71,[1317,3183,1360],{"emptyLinePlaceholder":1359},[1317,3185,3187,3190,3192,3194,3196,3198],{"class":1319,"line":3186},72,[1317,3188,3189],{"class":1378},"  add",[1317,3191,1528],{"class":1326},[1317,3193,1402],{"class":1401},[1317,3195,1405],{"class":1322},[1317,3197,1409],{"class":1408},[1317,3199,3200],{"class":1326},") {\n",[1317,3202,3204,3207,3209,3211,3213,3215],{"class":1319,"line":3203},73,[1317,3205,3206],{"class":1371},"    this",[1317,3208,2648],{"class":1326},[1317,3210,1525],{"class":1378},[1317,3212,1528],{"class":1326},[1317,3214,1500],{"class":1378},[1317,3216,1533],{"class":1326},[1317,3218,3220],{"class":1319,"line":3219},74,[1317,3221,3222],{"class":1326},"  }\n",[1317,3224,3226],{"class":1319,"line":3225},75,[1317,3227,1360],{"emptyLinePlaceholder":1359},[1317,3229,3231,3234,3236,3238,3240,3242],{"class":1319,"line":3230},76,[1317,3232,3233],{"class":1378},"  remove",[1317,3235,1528],{"class":1326},[1317,3237,1452],{"class":1401},[1317,3239,1405],{"class":1322},[1317,3241,1457],{"class":1371},[1317,3243,3200],{"class":1326},[1317,3245,3247,3249,3251,3253,3255,3258],{"class":1319,"line":3246},77,[1317,3248,3206],{"class":1371},[1317,3250,2648],{"class":1326},[1317,3252,1525],{"class":1378},[1317,3254,1528],{"class":1326},[1317,3256,3257],{"class":1378},"removeProduct",[1317,3259,3260],{"class":1326},"({ id }));\n",[1317,3262,3264],{"class":1319,"line":3263},78,[1317,3265,3222],{"class":1326},[1317,3267,3269],{"class":1319,"line":3268},79,[1317,3270,1360],{"emptyLinePlaceholder":1359},[1317,3272,3274,3277],{"class":1319,"line":3273},80,[1317,3275,3276],{"class":1378},"  clear",[1317,3278,3279],{"class":1326},"() {\n",[1317,3281,3283,3285,3287,3289,3291,3293],{"class":1319,"line":3282},81,[1317,3284,3206],{"class":1371},[1317,3286,2648],{"class":1326},[1317,3288,1525],{"class":1378},[1317,3290,1528],{"class":1326},[1317,3292,1504],{"class":1378},[1317,3294,3295],{"class":1326},"());\n",[1317,3297,3299],{"class":1319,"line":3298},82,[1317,3300,3222],{"class":1326},[1317,3302,3304],{"class":1319,"line":3303},83,[1317,3305,1611],{"class":1326},[1216,3307,3308,3309,3311,3312,3314],{},"Component 透過 ",[1220,3310,1511],{}," 發送 Action，由 Reducer 更新 State，再利用 ",[1220,3313,2622],{}," 取得 Selector 的結果。當資料改變時，畫面也會跟著更新。",[3316,3317,3318],"blockquote",{},[1216,3319,3320],{},[3321,3322,3326],"a",{"href":3323,"rel":3324},"https:\u002F\u002Fstackblitz.com\u002Fedit\u002Fldbybozv-cxcp9ywv?file=src%2Fapp%2Fcart%2Fcart.reducer.ts",[3325],"nofollow","Angular NgRx Store vs SignalStore，從 Action、Reducer 到直接更新 State - StackBlitz",[1251,3328,3330],{"id":3329},"ngrx-store-與-signalstore-的差異","NgRx Store 與 SignalStore 的差異",[1216,3332,3333,3334,2685,3336,1231,3338,3340],{},"回頭看之前的 SignalStore，我們不需要另外建立 Action、Reducer 與 Selector，而是透過 ",[1220,3335,1222],{},[1220,3337,1226],{},[1220,3339,1230],{}," 管理相關功能。",[1291,3342,3344],{"id":3343},"action-與-method","Action 與 Method",[1216,3346,3347,3348,3350],{},"傳統 NgRx Store 透過 ",[1220,3349,1511],{}," 發送 Action：",[1308,3352,3353],{"className":1310,"code":1515,"language":1312,"meta":1313,"style":1313},[1220,3354,3355],{"__ignoreMap":1313},[1317,3356,3357,3359,3361,3363,3365],{"class":1319,"line":340},[1317,3358,1522],{"class":1326},[1317,3360,1525],{"class":1378},[1317,3362,1528],{"class":1326},[1317,3364,1500],{"class":1378},[1317,3366,1533],{"class":1326},[1216,3368,3369,3370,3372],{},"SignalStore 則直接呼叫 ",[1220,3371,1230],{}," 定義的方法：",[1308,3374,3376],{"className":1310,"code":3375,"language":1312,"meta":1313,"style":1313},"store.add(product);\n",[1220,3377,3378],{"__ignoreMap":1313},[1317,3379,3380,3382,3385],{"class":1319,"line":340},[1317,3381,1522],{"class":1326},[1317,3383,3384],{"class":1378},"add",[1317,3386,3387],{"class":1326},"(product);\n",[1216,3389,3390],{},[1240,3391,3392],{},"Action 描述發生了什麼事件，而 Method 則代表呼叫 Store 提供的某個操作。",[1216,3394,3395],{},"同一個 Action 可以由不同 Reducer 處理，分別更新各自的 State，也能讓 Effect 監聽並處理非同步操作。",[1216,3397,3398],{},"SignalStore 則將操作邏輯封裝在 Method 中。如果需要跨功能協作，可以再透過 Service、RxJS 或其他方式協調。",[1216,3400,3401],{},[1286,3402],{"alt":3403,"src":3404},"NgRx Store 與 SignalStore 的 Action、Method 設計差異","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791798435\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2027%EF%BC%9ANgRx%20Store%20vs%20SignalStore%EF%BC%8C%E5%BE%9E%20Action%E3%80%81Reducer%20%E5%88%B0%E7%9B%B4%E6%8E%A5%E6%9B%B4%E6%96%B0%20State\u002Fngrx-store-vs-signal-store-v2.png",[1291,3406,3408],{"id":3407},"reducer-與-patchstate","Reducer 與 patchState()",[1216,3410,3411],{},"傳統 NgRx Store 透過 Reducer 更新 State：",[1308,3413,3415],{"className":1310,"code":3414,"language":1312,"meta":1313,"style":1313},"on(clearCart, state => ({\n  ...state,\n  items: []\n}))\n",[1220,3416,3417,3430,3437,3441],{"__ignoreMap":1313},[1317,3418,3419,3422,3424,3426,3428],{"class":1319,"line":340},[1317,3420,3421],{"class":1378},"on",[1317,3423,2089],{"class":1326},[1317,3425,1835],{"class":1401},[1317,3427,1874],{"class":1322},[1317,3429,2039],{"class":1326},[1317,3431,3432,3435],{"class":1319,"line":329},[1317,3433,3434],{"class":1322},"  ...",[1317,3436,2048],{"class":1326},[1317,3438,3439],{"class":1319,"line":318},[1317,3440,1790],{"class":1326},[1317,3442,3443],{"class":1319,"line":134},[1317,3444,3445],{"class":1326},"}))\n",[1216,3447,3448,3449,3451],{},"SignalStore 則可以在 Method 中使用 ",[1220,3450,1234],{},"：",[1308,3453,3455],{"className":1310,"code":3454,"language":1312,"meta":1313,"style":1313},"withMethods(store => ({\n  clear() {\n    patchState(store, { items: [] });\n  }\n}))\n",[1220,3456,3457,3471,3477,3485,3489],{"__ignoreMap":1313},[1317,3458,3459,3462,3464,3467,3469],{"class":1319,"line":340},[1317,3460,3461],{"class":1378},"withMethods",[1317,3463,1528],{"class":1326},[1317,3465,3466],{"class":1401},"store",[1317,3468,1874],{"class":1322},[1317,3470,2039],{"class":1326},[1317,3472,3473,3475],{"class":1319,"line":329},[1317,3474,3276],{"class":1378},[1317,3476,3279],{"class":1326},[1317,3478,3479,3482],{"class":1319,"line":318},[1317,3480,3481],{"class":1378},"    patchState",[1317,3483,3484],{"class":1326},"(store, { items: [] });\n",[1317,3486,3487],{"class":1319,"line":134},[1317,3488,3222],{"class":1326},[1317,3490,3491],{"class":1319,"line":122},[1317,3492,3445],{"class":1326},[1216,3494,3495,3496,1243],{},"Reducer 需要根據 Action 決定如何更新 State；SignalStore 則可以直接在 Method 中呼叫 ",[1220,3497,1234],{},[1216,3499,3500,3501,3503],{},"另外，",[1220,3502,1234],{}," 只需要指定要更新的屬性，不必手動展開整個 State。不過，更新陣列或物件時，仍然需要注意不可變性。",[1291,3505,3507],{"id":3506},"selector-與-computed","Selector 與 Computed",[1216,3509,3510,3511,3513],{},"傳統 NgRx Store 使用 ",[1220,3512,2174],{}," 計算衍生資料：",[1308,3515,3517],{"className":1310,"code":3516,"language":1312,"meta":1313,"style":1313},"export const selectCartTotal = createSelector(\n  selectCartItems,\n  items => items.reduce(\n    (sum, item) => sum + item.price * item.quantity,\n    0\n  )\n);\n",[1220,3518,3519,3533,3537,3549,3573,3577,3581],{"__ignoreMap":1313},[1317,3520,3521,3523,3525,3527,3529,3531],{"class":1319,"line":340},[1317,3522,1365],{"class":1322},[1317,3524,1368],{"class":1322},[1317,3526,2393],{"class":1371},[1317,3528,1375],{"class":1322},[1317,3530,2282],{"class":1378},[1317,3532,1382],{"class":1326},[1317,3534,3535],{"class":1319,"line":329},[1317,3536,2327],{"class":1326},[1317,3538,3539,3541,3543,3545,3547],{"class":1319,"line":318},[1317,3540,1667],{"class":1401},[1317,3542,1874],{"class":1322},[1317,3544,2336],{"class":1326},[1317,3546,1045],{"class":1378},[1317,3548,1382],{"class":1326},[1317,3550,3551,3553,3555,3557,3559,3561,3563,3565,3567,3569,3571],{"class":1319,"line":134},[1317,3552,2345],{"class":1326},[1317,3554,2348],{"class":1401},[1317,3556,2351],{"class":1326},[1317,3558,1918],{"class":1401},[1317,3560,2356],{"class":1326},[1317,3562,1846],{"class":1322},[1317,3564,2361],{"class":1326},[1317,3566,1947],{"class":1322},[1317,3568,2436],{"class":1326},[1317,3570,2439],{"class":1322},[1317,3572,2366],{"class":1326},[1317,3574,3575],{"class":1319,"line":122},[1317,3576,2371],{"class":1371},[1317,3578,3579],{"class":1319,"line":31},[1317,3580,2376],{"class":1326},[1317,3582,3583],{"class":1319,"line":17},[1317,3584,1417],{"class":1326},[1216,3586,3587,3588,3590,3591,3451],{},"SignalStore 則可以在 ",[1220,3589,1226],{}," 中使用 ",[1220,3592,3593],{},"computed()",[1308,3595,3597],{"className":1310,"code":3596,"language":1312,"meta":1313,"style":1313},"withComputed(({ items }) => ({\n  total: computed(() =>\n    items().reduce(\n      (sum, item) => sum + item.price * item.quantity,\n      0\n    )\n  )\n}))\n",[1220,3598,3599,3616,3630,3642,3667,3672,3677,3681],{"__ignoreMap":1313},[1317,3600,3601,3604,3607,3610,3612,3614],{"class":1319,"line":340},[1317,3602,3603],{"class":1378},"withComputed",[1317,3605,3606],{"class":1326},"(({ ",[1317,3608,3609],{"class":1401},"items",[1317,3611,1843],{"class":1326},[1317,3613,1846],{"class":1322},[1317,3615,2039],{"class":1326},[1317,3617,3618,3621,3624,3627],{"class":1319,"line":329},[1317,3619,3620],{"class":1326},"  total: ",[1317,3622,3623],{"class":1378},"computed",[1317,3625,3626],{"class":1326},"(() ",[1317,3628,3629],{"class":1322},"=>\n",[1317,3631,3632,3635,3638,3640],{"class":1319,"line":318},[1317,3633,3634],{"class":1378},"    items",[1317,3636,3637],{"class":1326},"().",[1317,3639,1045],{"class":1378},[1317,3641,1382],{"class":1326},[1317,3643,3644,3647,3649,3651,3653,3655,3657,3659,3661,3663,3665],{"class":1319,"line":134},[1317,3645,3646],{"class":1326},"      (",[1317,3648,2348],{"class":1401},[1317,3650,2351],{"class":1326},[1317,3652,1918],{"class":1401},[1317,3654,2356],{"class":1326},[1317,3656,1846],{"class":1322},[1317,3658,2361],{"class":1326},[1317,3660,1947],{"class":1322},[1317,3662,2436],{"class":1326},[1317,3664,2439],{"class":1322},[1317,3666,2366],{"class":1326},[1317,3668,3669],{"class":1319,"line":122},[1317,3670,3671],{"class":1371},"      0\n",[1317,3673,3674],{"class":1319,"line":31},[1317,3675,3676],{"class":1326},"    )\n",[1317,3678,3679],{"class":1319,"line":17},[1317,3680,2376],{"class":1326},[1317,3682,3683],{"class":1319,"line":110},[1317,3684,3445],{"class":1326},[1216,3686,3687],{},"兩者都有快取機制，但依賴追蹤方式不同。",[1216,3689,3690,3692,3693,3695],{},[1220,3691,2174],{}," 會根據輸入 Selector 的結果判斷是否需要重新計算，而 ",[1220,3694,3593],{}," 則會追蹤計算過程中實際讀取的 Signal。當依賴改變時，Computed 會將快取標記為失效，並在下次讀取時重新計算。",[1251,3697,3699],{"id":3698},"什麼時候適合使用哪一種","什麼時候適合使用哪一種？",[1216,3701,3702],{},"整理前面的差異：",[3704,3705,3706,3721],"table",{},[3707,3708,3709],"thead",{},[3710,3711,3712,3716,3718],"tr",{},[3713,3714,3715],"th",{},"比較項目",[3713,3717,1242],{},[3713,3719,3720],{},"SignalStore",[3722,3723,3724,3737,3750,3761,3774,3785,3799],"tbody",{},[3710,3725,3726,3730,3733],{},[3727,3728,3729],"td",{},"狀態定義",[3727,3731,3732],{},"Initial State",[3727,3734,3735],{},[1220,3736,1222],{},[3710,3738,3739,3742,3746],{},[3727,3740,3741],{},"更新入口",[3727,3743,3744],{},[1220,3745,1511],{},[3727,3747,3748],{},[1220,3749,1230],{},[3710,3751,3752,3755,3757],{},[3727,3753,3754],{},"更新方式",[3727,3756,1272],{},[3727,3758,3759],{},[1220,3760,1234],{},[3710,3762,3763,3766,3770],{},[3727,3764,3765],{},"衍生資料",[3727,3767,3768],{},[1220,3769,2174],{},[3727,3771,3772],{},[1220,3773,1226],{},[3710,3775,3776,3779,3782],{},[3727,3777,3778],{},"資料讀取",[3727,3780,3781],{},"Observable \u002F Signal",[3727,3783,3784],{},"Signal",[3710,3786,3787,3790,3793],{},[3727,3788,3789],{},"非同步操作",[3727,3791,3792],{},"Effects",[3727,3794,3795,3798],{},[1220,3796,3797],{},"rxMethod()"," \u002F async Method",[3710,3800,3801,3804,3807],{},[3727,3802,3803],{},"設計重點",[3727,3805,3806],{},"Action-driven",[3727,3808,3809],{},"Method-driven",[1216,3811,3812],{},"如果只是管理購物車、頁面資料或其他相對獨立的功能，SignalStore 可以將 State、Computed 與 Methods 集中管理，減少額外的程式結構。",[1216,3814,3815],{},"傳統 NgRx Store 則適合需要明確事件語意，或讓不同功能針對相同事件做出反應的情境。",[1216,3817,3818,3819,3822],{},"例如訂單建立成功後，可以發送一個 ",[1220,3820,3821],{},"checkoutSuccess"," Action。購物車 Reducer 收到後清空商品，訂單 Reducer 則更新訂單狀態；如果還需要發送通知，也能交由 Effect 處理。",[1216,3824,3825],{},"這些功能仍然需要知道共同的 Action，但不必直接呼叫彼此的方法。不過，如果業務操作需要嚴格的執行順序，就必須透過 Effect 或不同階段的 Action 明確安排，而不能只依賴多個功能監聽同一個事件。",[1216,3827,3828],{},"SignalStore 同樣能處理跨功能協作，只是通常需要透過 Service、RxJS 或其他架構協調。因此，實際選擇時還是要考慮功能複雜度、事件追蹤需求與團隊現有架構。",[1251,3830,3831],{"id":3831},"本日小結",[1216,3833,3834],{},"今天透過購物車範例，認識傳統 NgRx Store 的 Action、Reducer 與 Selector。Action 負責描述事件，Reducer 根據事件產生新的 State，而 Selector 則負責取得與計算資料。",[1216,3836,3837,3838,2685,3840,1231,3842,3844,3845,3847],{},"相較之下，SignalStore 使用 ",[1220,3839,1222],{},[1220,3841,1226],{},[1220,3843,1230],{}," 集中管理狀態與操作，並透過 ",[1220,3846,1234],{}," 更新資料，不需要額外定義 Action 與 Reducer。",[1216,3849,3850],{},"兩者的差異不只是 Observable 與 Signal，而是狀態更新的設計方式。NgRx Store 以 Action 為核心，讓不同功能可以回應相同事件；SignalStore 則透過 Method 封裝操作，讓相關邏輯集中在 Store 中。",[1216,3852,3853],{},"實際開發時不一定要擇一使用。如果功能相對獨立，可以考慮 SignalStore；如果系統需要明確的事件追蹤，或已經採用 NgRx Store 架構，傳統 Store 仍然是合適的選擇。",[1291,3855,3856],{"id":3856},"資料來源",[1259,3858,3859,3867,3875,3883,3891],{},[1262,3860,3861,3866],{},[3321,3862,3865],{"href":3863,"rel":3864},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fstore\u002Factions",[3325],"NgRx 官方文件：Actions","：Action 的定義與使用方式。",[1262,3868,3869,3874],{},[3321,3870,3873],{"href":3871,"rel":3872},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fstore\u002Freducers",[3325],"NgRx 官方文件：Reducers","：Reducer 的純函式特性與不可變更新。",[1262,3876,3877,3882],{},[3321,3878,3881],{"href":3879,"rel":3880},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fstore\u002Fconfiguration\u002Fruntime-checks",[3325],"NgRx 官方文件：Runtime checks","：State 與 Action 的執行階段檢查。",[1262,3884,3885,3890],{},[3321,3886,3889],{"href":3887,"rel":3888},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fstore\u002Fselectors",[3325],"NgRx 官方文件：Selectors","：Selector 的 Memoization 機制。",[1262,3892,3893,3451,3898,1231,3900,3902],{},[3321,3894,3897],{"href":3895,"rel":3896},"https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Fsignal-store",[3325],"NgRx 官方文件：SignalStore",[1220,3899,1230],{},[1220,3901,1234],{}," 的使用方式",[3904,3905,3906],"style",{},"html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}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 .sMOND, html code.shiki .sMOND{--shiki-default:#6F42C1;--shiki-dark:#6CB6FF}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}",{"title":1313,"searchDepth":134,"depth":134,"links":3908},[3909,3914,3918,3923,3924],{"id":1253,"depth":329,"text":1254,"children":3910},[3911,3912,3913],{"id":1293,"depth":318,"text":1294},{"id":1543,"depth":318,"text":1544},{"id":2161,"depth":318,"text":2162},{"id":2472,"depth":329,"text":2473,"children":3915},[3916,3917],{"id":2479,"depth":318,"text":2480},{"id":2605,"depth":318,"text":2606},{"id":3329,"depth":329,"text":3330,"children":3919},[3920,3921,3922],{"id":3343,"depth":318,"text":3344},{"id":3407,"depth":318,"text":3408},{"id":3506,"depth":318,"text":3507},{"id":3698,"depth":329,"text":3699},{"id":3831,"depth":329,"text":3831,"children":3925},[3926],{"id":3856,"depth":318,"text":3856},[11,13],"md",{},"---\ntitle: \"Day 27：NgRx Store vs SignalStore，從 Action、Reducer 到直接更新 State\"\ndate: 2026-10-11 20:06:41\ntags:\n  - \"iThome 鐵人賽\"\n  - \"2026 鐵人賽\"\n  - \"Angular\"\ncategories:\n  - \"iThome 鐵人賽\"\n  - \"Angular\"\ndescription: \"前幾天我們使用 NgRx SignalStore 實作購物車，透過 withState() 管理商品資料、withComputed() 計算總金額，再利用 withMethods() 與 patchState() 更新 …\"\ncanonical: https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423362\nseries:\n  name: \"2026 iThome 鐵人賽：Angular 22 Signal 進化論\"\n  url: https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F9939\n  day: 27\n---\n\n前幾天我們使用 NgRx SignalStore 實作購物車，透過 `withState()` 管理商品資料、`withComputed()` 計算總金額，再利用 `withMethods()` 與 `patchState()` 更新 State。\n\n不過，在 SignalStore 出現之前，NgRx 就已經提供了另一套狀態管理方案，也就是傳統的 **NgRx Store**。\n\nNgRx Store 採用 Redux 的設計概念，透過 Action 描述事件，再交由 Reducer 更新 State。這與 SignalStore 直接呼叫 Method 的方式不同。\n\n今天延續購物車範例，使用傳統 NgRx Store 重新實作一次，比較兩種方式在狀態管理上的差異。\n\n## 認識傳統 NgRx Store\n\nNgRx Store 主要由三個部分組成：\n\n- **Action**：描述發生了什麼事件。\n- **Reducer**：根據 Action 與目前的 State，產生新的 State。\n- **Selector**：取得 State 中需要的資料，或計算衍生資料。\n\n例如使用者點擊「加入購物車」，Component 會發送 Action，再由 Reducer 更新購物車 State，Selector 則負責取得畫面需要的資料。\n\n![NgRx Store 的 Action、Reducer、State 與 Selector 關係圖](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791719451\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2027%EF%BC%9ANgRx%20Store%20vs%20SignalStore%EF%BC%8C%E5%BE%9E%20Action%E3%80%81Reducer%20%E5%88%B0%E7%9B%B4%E6%8E%A5%E6%9B%B4%E6%96%B0%20State\u002Fngrx-store-overview.png)\n\n### 定義 Action\n\nAction 用來描述 Application 中發生的事件。NgRx 提供 `createAction()` 定義事件名稱，以及需要攜帶的資料。\n\n`cart.actions.ts`\n\n```ts\nimport { createAction, props } from '@ngrx\u002Fstore';\nimport type { Product } from '.\u002Fcart.types';\n\nexport const addProduct = createAction(\n  '[Cart] Add Product',\n  props\u003C{ product: Product }>()\n);\n\nexport const removeProduct = createAction(\n  '[Cart] Remove Product',\n  props\u003C{ id: number }>()\n);\n\nexport const clearCart = createAction(\n  '[Cart] Clear Cart'\n);\n```\n\n`props()` 用來定義 Action 需要攜帶的資料。例如 `addProduct` 需要提供商品，而 `clearCart` 不需要額外參數。\n\n定義完成後，可以透過 `dispatch()` 將 Action 發送給 Store：\n\n```ts\nstore.dispatch(addProduct({ product }));\n```\n\n這裡的 `addProduct()` 只負責建立 Action 物件，真正的狀態更新會交由 Reducer 處理。\n\n### 使用 Reducer 更新 State\n\nReducer 是一個 Pure Function（純函式），會根據目前的 State 與收到的 Action，回傳新的 State。\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\nexport interface CartState {\n  items: CartItem[];\n}\n```\n\n接著使用 `createReducer()` 建立購物車 Reducer。\n\n`cart.reducer.ts`\n\n```ts\nimport { createReducer, on } from '@ngrx\u002Fstore';\n\nimport {\n  addProduct,\n  removeProduct,\n  clearCart\n} from '.\u002Fcart.actions';\n\nimport type { CartState } from '.\u002Fcart.types';\n\nexport const initialState: CartState = {\n  items: []\n};\n\nexport const cartReducer = createReducer(\n  initialState,\n\n  on(addProduct, (state, { product }) => {\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 { ...state, items };\n  }),\n\n  on(removeProduct, (state, { id }) => ({\n    ...state,\n    items: state.items.filter(item => item.id !== id)\n  })),\n\n  on(clearCart, state => ({\n    ...state,\n    items: []\n  }))\n);\n```\n\n`createReducer()` 用來建立 Reducer，`on()` 則指定不同 Action 對應的更新邏輯。\n\n以 `addProduct` 為例，會先檢查商品是否已經存在。如果存在就增加數量，否則加入新商品。\n\n這裡要注意，**Reducer 必須遵守不可變性（Immutability）**，不能直接修改原本的 State，而是要建立並回傳新的資料。\n\n例如使用 `state.items.push()` 會直接修改原本的陣列，可能讓依賴參考比較的 Selector 無法正確重新計算。在 NgRx 預設啟用的開發環境 Runtime Checks 下，也可能因此拋出錯誤。\n\n所以範例使用展開運算子、`map()` 與 `filter()` 建立新資料。Reducer 也必須保持純函式的特性，不應在裡面呼叫 API、操作 DOM 或執行其他副作用。\n\n### 使用 Selector 取得資料\n\n除了更新 State，購物車還需要計算商品數量與總金額。\n\nNgRx 提供 `createFeatureSelector()` 與 `createSelector()`，讓我們取得指定的 State，並計算衍生資料。\n\n`cart.selectors.ts`\n\n```ts\nimport {\n  createFeatureSelector,\n  createSelector\n} from '@ngrx\u002Fstore';\n\nimport type { CartState } from '.\u002Fcart.types';\n\nexport const selectCartState =\n  createFeatureSelector\u003CCartState>('cart');\n\nexport const selectCartItems = createSelector(\n  selectCartState,\n  state => state.items\n);\n\nexport const selectCartCount = createSelector(\n  selectCartItems,\n  items => items.reduce(\n    (sum, item) => sum + item.quantity,\n    0\n  )\n);\n\nexport const selectCartTotal = createSelector(\n  selectCartItems,\n  items => items.reduce(\n    (sum, item) => sum + item.price * item.quantity,\n    0\n  )\n);\n```\n\n`createFeatureSelector('cart')` 用來取得 Store 中 `cart` 這個 Key 對應的 State，而 `createSelector()` 可以根據其他 Selector 的結果取得或計算資料。\n\nSelector 也具備 Memoization（記憶化）機制。當輸入沒有改變時，可以重複使用之前的計算結果，避免不必要的運算。\n\n## 在 Angular 使用 NgRx Store\n\nAction、Reducer 與 Selector 準備完成後，就可以將 Store 註冊到 Angular Application。\n\n### 註冊 Store\n\n`app.config.ts`\n\n```ts\nimport { ApplicationConfig } from '@angular\u002Fcore';\nimport { provideStore } from '@ngrx\u002Fstore';\n\nimport { cartReducer } from '.\u002Fcart\u002Fcart.reducer';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideStore({\n      cart: cartReducer\n    })\n  ]\n};\n```\n\n這裡透過 `provideStore()` 註冊購物車 Reducer，並使用 `cart` 作為 State 的 Key，與前面 `createFeatureSelector('cart')` 對應。\n\n### Component 使用 Store\n\n完成註冊後，就能透過 Angular DI 注入 `Store`。\n\n傳統 NgRx Store 可以使用 `select()` 取得 Observable，也能透過 `selectSignal()` 取得 Signal。例如讀取購物車總金額：\n\n```ts\n\u002F\u002F Observable\nreadonly total$ = this.store.select(selectCartTotal);\n\n\u002F\u002F Signal\nreadonly total = this.store.selectSignal(selectCartTotal);\n```\n\n如果需要搭配 RxJS 的 `map()`、`filter()` 或 `combineLatest()` 處理資料，可以使用 `select()`；如果希望在 Template 直接以 Signal 讀取資料，則可以使用 `selectSignal()`。\n\n這次為了與之前的 SignalStore 比較，我們使用 `selectSignal()`。\n\n`cart.ts`\n\n```ts\nimport { Component, inject } from '@angular\u002Fcore';\nimport { Store } from '@ngrx\u002Fstore';\n\nimport {\n  addProduct,\n  removeProduct,\n  clearCart\n} from '.\u002Fcart.actions';\n\nimport {\n  selectCartItems,\n  selectCartCount,\n  selectCartTotal\n} from '.\u002Fcart.selectors';\n\nimport type { Product } from '.\u002Fcart.types';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003Cmain>\n      \u003Ch2>商品列表\u003C\u002Fh2>\n\n      \u003Cbutton (click)=\"add({\n        id: 1,\n        name: '鍵盤',\n        price: 1200\n      })\">\n        加入鍵盤\n      \u003C\u002Fbutton>\n\n      \u003Cbutton (click)=\"add({\n        id: 2,\n        name: '滑鼠',\n        price: 800\n      })\">\n        加入滑鼠\n      \u003C\u002Fbutton>\n\n      \u003Chr \u002F>\n\n      \u003Ch2>購物車\u003C\u002Fh2>\n\n      @for (item of items(); track item.id) {\n        \u003Cp>\n          {{ item.name }} × {{ item.quantity }}\n\n          \u003Cbutton (click)=\"remove(item.id)\">\n            移除\n          \u003C\u002Fbutton>\n        \u003C\u002Fp>\n      } @empty {\n        \u003Cp>購物車目前沒有商品\u003C\u002Fp>\n      }\n\n      \u003Cp>商品數量：{{ count() }}\u003C\u002Fp>\n      \u003Cp>總金額：{{ total() }} 元\u003C\u002Fp>\n\n      \u003Cbutton (click)=\"clear()\">\n        清空購物車\n      \u003C\u002Fbutton>\n    \u003C\u002Fmain>\n  `\n})\nexport class CartComponent {\n  private readonly store = inject(Store);\n\n  readonly items = this.store.selectSignal(selectCartItems);\n  readonly count = this.store.selectSignal(selectCartCount);\n  readonly total = this.store.selectSignal(selectCartTotal);\n\n  add(product: Product) {\n    this.store.dispatch(addProduct({ product }));\n  }\n\n  remove(id: number) {\n    this.store.dispatch(removeProduct({ id }));\n  }\n\n  clear() {\n    this.store.dispatch(clearCart());\n  }\n}\n```\n\nComponent 透過 `dispatch()` 發送 Action，由 Reducer 更新 State，再利用 `selectSignal()` 取得 Selector 的結果。當資料改變時，畫面也會跟著更新。\n\n> [Angular NgRx Store vs SignalStore，從 Action、Reducer 到直接更新 State - StackBlitz](https:\u002F\u002Fstackblitz.com\u002Fedit\u002Fldbybozv-cxcp9ywv?file=src%2Fapp%2Fcart%2Fcart.reducer.ts)\n\n## NgRx Store 與 SignalStore 的差異\n\n回頭看之前的 SignalStore，我們不需要另外建立 Action、Reducer 與 Selector，而是透過 `withState()`、`withComputed()` 與 `withMethods()` 管理相關功能。\n\n### Action 與 Method\n\n傳統 NgRx Store 透過 `dispatch()` 發送 Action：\n\n```ts\nstore.dispatch(addProduct({ product }));\n```\n\nSignalStore 則直接呼叫 `withMethods()` 定義的方法：\n\n```ts\nstore.add(product);\n```\n\n**Action 描述發生了什麼事件，而 Method 則代表呼叫 Store 提供的某個操作。**\n\n同一個 Action 可以由不同 Reducer 處理，分別更新各自的 State，也能讓 Effect 監聽並處理非同步操作。\n\nSignalStore 則將操作邏輯封裝在 Method 中。如果需要跨功能協作，可以再透過 Service、RxJS 或其他方式協調。\n\n![NgRx Store 與 SignalStore 的 Action、Method 設計差異](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791798435\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2027%EF%BC%9ANgRx%20Store%20vs%20SignalStore%EF%BC%8C%E5%BE%9E%20Action%E3%80%81Reducer%20%E5%88%B0%E7%9B%B4%E6%8E%A5%E6%9B%B4%E6%96%B0%20State\u002Fngrx-store-vs-signal-store-v2.png)\n\n### Reducer 與 patchState()\n\n傳統 NgRx Store 透過 Reducer 更新 State：\n\n```ts\non(clearCart, state => ({\n  ...state,\n  items: []\n}))\n```\n\nSignalStore 則可以在 Method 中使用 `patchState()`：\n\n```ts\nwithMethods(store => ({\n  clear() {\n    patchState(store, { items: [] });\n  }\n}))\n```\n\nReducer 需要根據 Action 決定如何更新 State；SignalStore 則可以直接在 Method 中呼叫 `patchState()`。\n\n另外，`patchState()` 只需要指定要更新的屬性，不必手動展開整個 State。不過，更新陣列或物件時，仍然需要注意不可變性。\n\n### Selector 與 Computed\n\n傳統 NgRx Store 使用 `createSelector()` 計算衍生資料：\n\n```ts\nexport const selectCartTotal = createSelector(\n  selectCartItems,\n  items => items.reduce(\n    (sum, item) => sum + item.price * item.quantity,\n    0\n  )\n);\n```\n\nSignalStore 則可以在 `withComputed()` 中使用 `computed()`：\n\n```ts\nwithComputed(({ items }) => ({\n  total: computed(() =>\n    items().reduce(\n      (sum, item) => sum + item.price * item.quantity,\n      0\n    )\n  )\n}))\n```\n\n兩者都有快取機制，但依賴追蹤方式不同。\n\n`createSelector()` 會根據輸入 Selector 的結果判斷是否需要重新計算，而 `computed()` 則會追蹤計算過程中實際讀取的 Signal。當依賴改變時，Computed 會將快取標記為失效，並在下次讀取時重新計算。\n\n## 什麼時候適合使用哪一種？\n\n整理前面的差異：\n\n| 比較項目 | NgRx Store | SignalStore |\n| --- | --- | --- |\n| 狀態定義 | Initial State | `withState()` |\n| 更新入口 | `dispatch()` | `withMethods()` |\n| 更新方式 | Reducer | `patchState()` |\n| 衍生資料 | `createSelector()` | `withComputed()` |\n| 資料讀取 | Observable \u002F Signal | Signal |\n| 非同步操作 | Effects | `rxMethod()` \u002F async Method |\n| 設計重點 | Action-driven | Method-driven |\n\n如果只是管理購物車、頁面資料或其他相對獨立的功能，SignalStore 可以將 State、Computed 與 Methods 集中管理，減少額外的程式結構。\n\n傳統 NgRx Store 則適合需要明確事件語意，或讓不同功能針對相同事件做出反應的情境。\n\n例如訂單建立成功後，可以發送一個 `checkoutSuccess` Action。購物車 Reducer 收到後清空商品，訂單 Reducer 則更新訂單狀態；如果還需要發送通知，也能交由 Effect 處理。\n\n這些功能仍然需要知道共同的 Action，但不必直接呼叫彼此的方法。不過，如果業務操作需要嚴格的執行順序，就必須透過 Effect 或不同階段的 Action 明確安排，而不能只依賴多個功能監聽同一個事件。\n\nSignalStore 同樣能處理跨功能協作，只是通常需要透過 Service、RxJS 或其他架構協調。因此，實際選擇時還是要考慮功能複雜度、事件追蹤需求與團隊現有架構。\n\n## 本日小結\n\n今天透過購物車範例，認識傳統 NgRx Store 的 Action、Reducer 與 Selector。Action 負責描述事件，Reducer 根據事件產生新的 State，而 Selector 則負責取得與計算資料。\n\n相較之下，SignalStore 使用 `withState()`、`withComputed()` 與 `withMethods()` 集中管理狀態與操作，並透過 `patchState()` 更新資料，不需要額外定義 Action 與 Reducer。\n\n兩者的差異不只是 Observable 與 Signal，而是狀態更新的設計方式。NgRx Store 以 Action 為核心，讓不同功能可以回應相同事件；SignalStore 則透過 Method 封裝操作，讓相關邏輯集中在 Store 中。\n\n實際開發時不一定要擇一使用。如果功能相對獨立，可以考慮 SignalStore；如果系統需要明確的事件追蹤，或已經採用 NgRx Store 架構，傳統 Store 仍然是合適的選擇。\n\n### 資料來源\n\n- [NgRx 官方文件：Actions](https:\u002F\u002Fngrx.io\u002Fguide\u002Fstore\u002Factions)：Action 的定義與使用方式。\n- [NgRx 官方文件：Reducers](https:\u002F\u002Fngrx.io\u002Fguide\u002Fstore\u002Freducers)：Reducer 的純函式特性與不可變更新。\n- [NgRx 官方文件：Runtime checks](https:\u002F\u002Fngrx.io\u002Fguide\u002Fstore\u002Fconfiguration\u002Fruntime-checks)：State 與 Action 的執行階段檢查。\n- [NgRx 官方文件：Selectors](https:\u002F\u002Fngrx.io\u002Fguide\u002Fstore\u002Fselectors)：Selector 的 Memoization 機制。\n- [NgRx 官方文件：SignalStore](https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals\u002Fsignal-store)：`withMethods()` 與 `patchState()` 的使用方式\n",{"title":48,"description":49},{"name":20,"url":21,"day":56},{"loc":47,"images":3934,"lastmod":50},[3935,3936],{"loc":1289},{"loc":3404},"ithome-2026-day-27","2026-10-11-ithome-2026-day-27",[11,12,13],"7TdLGLVocnhiDNOQEnyyUdorX4vq4WY8ddPs3ru5hEQ",1792575169815]