[{"data":1,"prerenderedAt":3729},["ShallowReactive",2],{"all-posts":3,"post:\u002Fithome-2026-day-23":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":93,"body":1212,"canonical":99,"categories":3711,"date":95,"description":94,"draft":15,"extension":3712,"meta":3713,"navigation":2020,"ogImage":9,"path":92,"rawbody":3714,"readingTime":17,"robots":9,"schemaOrg":9,"seo":3715,"series":3716,"sitemap":3717,"slug":3725,"stem":3726,"tags":3727,"updated":9,"wordCount":98,"__hash__":3728},"posts\u002F2026-10-07-ithome-2026-day-23.md",{"type":1213,"value":1214,"toc":3695},"minimark",[1215,1218,1231,1234,1349,1352,1366,1369,1383,1386,1389,1395,1402,1407,1413,1416,1452,1463,1474,1480,1496,1499,1506,1519,1523,1530,1549,1552,1558,1561,1567,1572,1576,1579,1590,1614,1621,1637,1644,1665,1672,1687,1693,1696,1702,1706,1711,1781,1787,1790,1806,1816,1820,1823,1928,1934,1949,1952,1955,2330,2339,2343,2348,2398,2404,2407,2435,2441,2446,2542,2549,2579,2589,2592,2607,2610,2614,2617,2620,2637,2640,2777,2783,2803,2806,2809,2812,2827,2833,2840,2862,2865,2869,2878,2945,2953,2961,3040,3045,3054,3060,3064,3067,3162,3165,3169,3175,3364,3370,3378,3384,3390,3403,3413,3419,3424,3429,3486,3539,3543,3546,3568,3578,3581,3586,3591,3594,3597,3600,3606,3615,3624,3627,3631,3691],[1216,1217,94],"p",{},[1219,1220,1221,1225,1228],"ul",{},[1222,1223,1224],"li",{},"病患列表",[1222,1226,1227],{},"掛號管理",[1222,1229,1230],{},"系統設定",[1216,1232,1233],{},"每個頁籤可能都對應不同的 Route：",[1235,1236,1241],"pre",{"className":1237,"code":1238,"language":1239,"meta":1240,"style":1240},"language-ts shiki shiki-themes github-light github-dark-dimmed","export const routes: Routes = [\n  {\n    path: 'patients',\n    component: PatientsComponent,\n  },\n  {\n    path: 'registrations',\n    component: RegistrationsComponent,\n  },\n  {\n    path: 'settings',\n    component: SettingsComponent,\n  },\n];\n","ts","",[1242,1243,1244,1273,1278,1290,1295,1300,1304,1313,1318,1322,1326,1335,1340,1344],"code",{"__ignoreMap":1240},[1245,1246,1248,1252,1255,1259,1262,1266,1269],"span",{"class":1247,"line":340},"line",[1245,1249,1251],{"class":1250},"s7YZ4","export",[1245,1253,1254],{"class":1250}," const",[1245,1256,1258],{"class":1257},"sviXB"," routes",[1245,1260,1261],{"class":1250},":",[1245,1263,1265],{"class":1264},"sOLd2"," Routes",[1245,1267,1268],{"class":1250}," =",[1245,1270,1272],{"class":1271},"sYgZi"," [\n",[1245,1274,1275],{"class":1247,"line":329},[1245,1276,1277],{"class":1271},"  {\n",[1245,1279,1280,1283,1287],{"class":1247,"line":318},[1245,1281,1282],{"class":1271},"    path: ",[1245,1284,1286],{"class":1285},"s-HuK","'patients'",[1245,1288,1289],{"class":1271},",\n",[1245,1291,1292],{"class":1247,"line":134},[1245,1293,1294],{"class":1271},"    component: PatientsComponent,\n",[1245,1296,1297],{"class":1247,"line":122},[1245,1298,1299],{"class":1271},"  },\n",[1245,1301,1302],{"class":1247,"line":31},[1245,1303,1277],{"class":1271},[1245,1305,1306,1308,1311],{"class":1247,"line":17},[1245,1307,1282],{"class":1271},[1245,1309,1310],{"class":1285},"'registrations'",[1245,1312,1289],{"class":1271},[1245,1314,1315],{"class":1247,"line":110},[1245,1316,1317],{"class":1271},"    component: RegistrationsComponent,\n",[1245,1319,1320],{"class":1247,"line":76},[1245,1321,1299],{"class":1271},[1245,1323,1324],{"class":1247,"line":247},[1245,1325,1277],{"class":1271},[1245,1327,1328,1330,1333],{"class":1247,"line":236},[1245,1329,1282],{"class":1271},[1245,1331,1332],{"class":1285},"'settings'",[1245,1334,1289],{"class":1271},[1245,1336,1337],{"class":1247,"line":225},[1245,1338,1339],{"class":1271},"    component: SettingsComponent,\n",[1245,1341,1342],{"class":1247,"line":214},[1245,1343,1299],{"class":1271},[1245,1345,1346],{"class":1247,"line":203},[1245,1347,1348],{"class":1271},"];\n",[1216,1350,1351],{},"假設現在停在病患列表，而且已經操作了一段時間：",[1219,1353,1354,1357,1360,1363],{},[1222,1355,1356],{},"搜尋：王小明",[1222,1358,1359],{},"狀態：看診中",[1222,1361,1362],{},"頁數：第 3 頁",[1222,1364,1365],{},"Scroll 已經滑到中間",[1216,1367,1368],{},"接著切到「掛號管理」，之後再切回「病患列表」。",[1216,1370,1371,1372,1375,1376,1379,1380,1382],{},"一般情況下，離開 ",[1242,1373,1374],{},"\u002Fpatients"," 時，",[1242,1377,1378],{},"PatientsComponent"," 會被銷毀；再次進入時，Angular 會建立新的 ",[1242,1381,1378],{},"。",[1216,1384,1385],{},"如果搜尋條件、頁碼或其他畫面狀態只存在 Component 裡，這些資料也會跟著消失。",[1216,1387,1388],{},"頁籤式操作通常希望使用者切到其他功能後，原本的頁面先保留下來，等切回來時可以接著操作。",[1216,1390,1391,1392,1382],{},"這時就可以使用 ",[1242,1393,1394],{},"RouteReuseStrategy",[1216,1396,1397],{},[1398,1399],"img",{"alt":1400,"src":1401},"頁籤切換與畫面狀態保留","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355942\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Ftab-switch-state-preserved.png",[1403,1404,1406],"h2",{"id":1405},"angular-原本就有-route-reuse","Angular 原本就有 Route Reuse",[1216,1408,1409,1410,1412],{},"在介紹自訂 ",[1242,1411,1394],{}," 之前，要先知道 Angular Router 本來就有 Route Reuse 的概念。",[1216,1414,1415],{},"例如：",[1235,1417,1419],{"className":1237,"code":1418,"language":1239,"meta":1240,"style":1240},"{\n  path: 'users\u002F:id',\n  component: UserComponent,\n}\n",[1242,1420,1421,1426,1439,1447],{"__ignoreMap":1240},[1245,1422,1423],{"class":1247,"line":340},[1245,1424,1425],{"class":1271},"{\n",[1245,1427,1428,1431,1434,1437],{"class":1247,"line":329},[1245,1429,1430],{"class":1264},"  path",[1245,1432,1433],{"class":1271},": ",[1245,1435,1436],{"class":1285},"'users\u002F:id'",[1245,1438,1289],{"class":1271},[1245,1440,1441,1444],{"class":1247,"line":318},[1245,1442,1443],{"class":1264},"  component",[1245,1445,1446],{"class":1271},": UserComponent,\n",[1245,1448,1449],{"class":1247,"line":134},[1245,1450,1451],{"class":1271},"}\n",[1216,1453,1454,1455,1458,1459,1462],{},"當路由從 ",[1242,1456,1457],{},"\u002Fusers\u002F1"," 切換到 ",[1242,1460,1461],{},"\u002Fusers\u002F2","，雖然 URL 不同，但兩邊使用的是同一份 Route Config。",[1216,1464,1465,1466,1469,1470,1473],{},"Angular 會繼續使用目前的 ",[1242,1467,1468],{},"UserComponent","，不會因為 ",[1242,1471,1472],{},"id"," 改變就重新建立一個新的 Component。",[1216,1475,1476,1479],{},[1242,1477,1478],{},"BaseRouteReuseStrategy"," 預設會判斷：",[1235,1481,1483],{"className":1237,"code":1482,"language":1239,"meta":1240,"style":1240},"future.routeConfig === curr.routeConfig\n",[1242,1484,1485],{"__ignoreMap":1240},[1245,1486,1487,1490,1493],{"class":1247,"line":340},[1245,1488,1489],{"class":1271},"future.routeConfig ",[1245,1491,1492],{"class":1250},"===",[1245,1494,1495],{"class":1271}," curr.routeConfig\n",[1216,1497,1498],{},"只要前後使用的是同一份 Route Config，就可以直接 reuse。",[1216,1500,1501,1502,1505],{},"因此 Params、Query Params，甚至 URL ",[1242,1503,1504],{},"#"," 後面的 Fragment 發生改變，都不代表 Component 一定會重新建立。",[1216,1507,1508,1509,1511,1512,1511,1515,1518],{},"這篇要處理的是另一種情況：在 ",[1242,1510,1374],{},"、",[1242,1513,1514],{},"\u002Fregistrations",[1242,1516,1517],{},"\u002Fsettings"," 這些不同 Route 之間切換時，希望其中某些頁面離開後仍然保留原本的 Component。",[1403,1520,1522],{"id":1521},"detach-和-destroy-的差別","detach 和 destroy 的差別",[1216,1524,1525,1526,1529],{},"Angular Router 會透過 ",[1242,1527,1528],{},"RouterOutlet"," 顯示目前 Route 對應的 Component：",[1235,1531,1535],{"className":1532,"code":1533,"language":1534,"meta":1240,"style":1240},"language-html shiki shiki-themes github-light github-dark-dimmed","\u003Crouter-outlet \u002F>\n","html",[1242,1536,1537],{"__ignoreMap":1240},[1245,1538,1539,1542,1546],{"class":1247,"line":340},[1245,1540,1541],{"class":1271},"\u003C",[1245,1543,1545],{"class":1544},"sIAta","router-outlet",[1245,1547,1548],{"class":1271}," \u002F>\n",[1216,1550,1551],{},"一般離開 Route 時，目前的 Component 會被 deactivate 並銷毀。",[1216,1553,1554,1555,1557],{},"如果希望保留這個 Route，可以改成 detach。Component 會先從 ",[1242,1556,1528],{}," 中取下，原本的 instance 則繼續保留。",[1216,1559,1560],{},"之後再次進入這個 Route 時，再透過 attach 把保存的內容接回來。",[1216,1562,1563],{},[1398,1564],{"alt":1565,"src":1566},"一般切換 vs Route Reuse","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355945\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Fnormal-vs-route-reuse.png",[1216,1568,1569,1571],{},[1242,1570,1394],{}," 就是用來決定哪些 Route 離開時直接銷毀，哪些 Route 要先保存起來。",[1403,1573,1575],{"id":1574},"這個概念和-view-的-detach-很像","這個概念和 View 的 detach 很像",[1216,1577,1578],{},"Angular 的 View API 也有類似設計。",[1216,1580,1581,1582,1585,1586,1589],{},"如果手上有一個 ",[1242,1583,1584],{},"ComponentRef","，可以先透過 ",[1242,1587,1588],{},"indexOf()"," 找到這個 Component 對應的 View 在 Container 中的位置：",[1235,1591,1593],{"className":1237,"code":1592,"language":1239,"meta":1240,"style":1240},"const index = viewContainerRef.indexOf(componentRef.hostView);\n",[1242,1594,1595],{"__ignoreMap":1240},[1245,1596,1597,1599,1602,1604,1607,1611],{"class":1247,"line":340},[1245,1598,1029],{"class":1250},[1245,1600,1601],{"class":1257}," index",[1245,1603,1268],{"class":1250},[1245,1605,1606],{"class":1271}," viewContainerRef.",[1245,1608,1610],{"class":1609},"sPO5f","indexOf",[1245,1612,1613],{"class":1271},"(componentRef.hostView);\n",[1216,1615,1616,1617,1620],{},"使用 ",[1242,1618,1619],{},"remove()"," 會把這個 View 從 Container 中移除並銷毀：",[1235,1622,1624],{"className":1237,"code":1623,"language":1239,"meta":1240,"style":1240},"viewContainerRef.remove(index);\n",[1242,1625,1626],{"__ignoreMap":1240},[1245,1627,1628,1631,1634],{"class":1247,"line":340},[1245,1629,1630],{"class":1271},"viewContainerRef.",[1245,1632,1633],{"class":1609},"remove",[1245,1635,1636],{"class":1271},"(index);\n",[1216,1638,1639,1640,1643],{},"如果只想暫時把 View 取下來，同時保留 View 本身，可以改用 ",[1242,1641,1642],{},"detach()","：",[1235,1645,1647],{"className":1237,"code":1646,"language":1239,"meta":1240,"style":1240},"const view = viewContainerRef.detach(index);\n",[1242,1648,1649],{"__ignoreMap":1240},[1245,1650,1651,1653,1656,1658,1660,1663],{"class":1247,"line":340},[1245,1652,1029],{"class":1250},[1245,1654,1655],{"class":1257}," view",[1245,1657,1268],{"class":1250},[1245,1659,1606],{"class":1271},[1245,1661,1662],{"class":1609},"detach",[1245,1664,1636],{"class":1271},[1216,1666,1667,1668,1671],{},"之後還能透過 ",[1242,1669,1670],{},"insert()"," 把同一個 View 放回去：",[1235,1673,1675],{"className":1237,"code":1674,"language":1239,"meta":1240,"style":1240},"viewContainerRef.insert(view);\n",[1242,1676,1677],{"__ignoreMap":1240},[1245,1678,1679,1681,1684],{"class":1247,"line":340},[1245,1680,1630],{"class":1271},[1245,1682,1683],{"class":1609},"insert",[1245,1685,1686],{"class":1271},"(view);\n",[1216,1688,1689],{},[1398,1690],{"alt":1691,"src":1692},"Detach 概念對照：View 與 Route API","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791358207\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Fdetach-concept-comparison.png",[1216,1694,1695],{},"兩邊不是同一套 API，但概念很接近：",[1697,1698,1699],"blockquote",{},[1216,1700,1701],{},"暫時從目前的位置移除，不代表一定要把它銷毀。",[1403,1703,1705],{"id":1704},"routereusestrategy-負責什麼","RouteReuseStrategy 負責什麼？",[1216,1707,1708,1710],{},[1242,1709,1394],{}," 主要會處理五個方法：",[1712,1713,1714,1727],"table",{},[1715,1716,1717],"thead",{},[1718,1719,1720,1724],"tr",{},[1721,1722,1723],"th",{},"方法",[1721,1725,1726],{},"用途",[1728,1729,1730,1741,1751,1761,1771],"tbody",{},[1718,1731,1732,1738],{},[1733,1734,1735],"td",{},[1242,1736,1737],{},"shouldDetach()",[1733,1739,1740],{},"離開時判斷要不要保存",[1718,1742,1743,1748],{},[1733,1744,1745],{},[1242,1746,1747],{},"store()",[1733,1749,1750],{},"保存 detach 下來的 Route",[1718,1752,1753,1758],{},[1733,1754,1755],{},[1242,1756,1757],{},"shouldAttach()",[1733,1759,1760],{},"進入時檢查是否有之前保存的 Route",[1718,1762,1763,1768],{},[1733,1764,1765],{},[1242,1766,1767],{},"retrieve()",[1733,1769,1770],{},"取得之前保存的 Route",[1718,1772,1773,1778],{},[1733,1774,1775],{},[1242,1776,1777],{},"shouldReuseRoute()",[1733,1779,1780],{},"判斷前後 Route 是否直接沿用",[1216,1782,1783],{},[1398,1784],{"alt":1785,"src":1786},"RouteReuseStrategy 五個方法","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355951\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Froute-reuse-strategy-methods.png",[1216,1788,1789],{},"前四個方法可以分成兩組：",[1219,1791,1792,1799],{},[1222,1793,1794,1511,1796,1798],{},[1242,1795,1737],{},[1242,1797,1747],{},"：離開 Route 時判斷是否保存，並把內容存起來",[1222,1800,1801,1511,1803,1805],{},[1242,1802,1757],{},[1242,1804,1767],{},"：再次進入時檢查是否有保存內容，並把它取回來",[1216,1807,1808,1810,1811,1458,1813,1815],{},[1242,1809,1777],{},"：負責判斷這次 Navigation 是否可以直接沿用目前的 Route。例如從 ",[1242,1812,1457],{},[1242,1814,1461],{}," 時，兩邊使用的是同一份 Route Config，因此可以直接 reuse。",[1403,1817,1819],{"id":1818},"建立自己的-routereusestrategy","建立自己的 RouteReuseStrategy",[1216,1821,1822],{},"通常只需要保留特定頁面，可以先透過 Route Data 標記：",[1235,1824,1826],{"className":1237,"code":1825,"language":1239,"meta":1240,"style":1240},"export const routes: Routes = [\n  {\n    path: 'patients',\n    component: PatientsComponent,\n    data: {\n      reuse: true,\n    },\n  },\n  {\n    path: 'registrations',\n    component: RegistrationsComponent,\n  },\n  {\n    path: 'settings',\n    component: SettingsComponent,\n  },\n];\n",[1242,1827,1828,1844,1848,1856,1860,1865,1875,1880,1884,1888,1896,1900,1904,1908,1916,1920,1924],{"__ignoreMap":1240},[1245,1829,1830,1832,1834,1836,1838,1840,1842],{"class":1247,"line":340},[1245,1831,1251],{"class":1250},[1245,1833,1254],{"class":1250},[1245,1835,1258],{"class":1257},[1245,1837,1261],{"class":1250},[1245,1839,1265],{"class":1264},[1245,1841,1268],{"class":1250},[1245,1843,1272],{"class":1271},[1245,1845,1846],{"class":1247,"line":329},[1245,1847,1277],{"class":1271},[1245,1849,1850,1852,1854],{"class":1247,"line":318},[1245,1851,1282],{"class":1271},[1245,1853,1286],{"class":1285},[1245,1855,1289],{"class":1271},[1245,1857,1858],{"class":1247,"line":134},[1245,1859,1294],{"class":1271},[1245,1861,1862],{"class":1247,"line":122},[1245,1863,1864],{"class":1271},"    data: {\n",[1245,1866,1867,1870,1873],{"class":1247,"line":31},[1245,1868,1869],{"class":1271},"      reuse: ",[1245,1871,1872],{"class":1257},"true",[1245,1874,1289],{"class":1271},[1245,1876,1877],{"class":1247,"line":17},[1245,1878,1879],{"class":1271},"    },\n",[1245,1881,1882],{"class":1247,"line":110},[1245,1883,1299],{"class":1271},[1245,1885,1886],{"class":1247,"line":76},[1245,1887,1277],{"class":1271},[1245,1889,1890,1892,1894],{"class":1247,"line":247},[1245,1891,1282],{"class":1271},[1245,1893,1310],{"class":1285},[1245,1895,1289],{"class":1271},[1245,1897,1898],{"class":1247,"line":236},[1245,1899,1317],{"class":1271},[1245,1901,1902],{"class":1247,"line":225},[1245,1903,1299],{"class":1271},[1245,1905,1906],{"class":1247,"line":214},[1245,1907,1277],{"class":1271},[1245,1909,1910,1912,1914],{"class":1247,"line":203},[1245,1911,1282],{"class":1271},[1245,1913,1332],{"class":1285},[1245,1915,1289],{"class":1271},[1245,1917,1918],{"class":1247,"line":192},[1245,1919,1339],{"class":1271},[1245,1921,1922],{"class":1247,"line":181},[1245,1923,1299],{"class":1271},[1245,1925,1926],{"class":1247,"line":170},[1245,1927,1348],{"class":1271},[1216,1929,1930,1931,1933],{},"這裡只有 ",[1242,1932,1374],{}," 設定：",[1235,1935,1937],{"className":1237,"code":1936,"language":1239,"meta":1240,"style":1240},"reuse: true\n",[1242,1938,1939],{"__ignoreMap":1240},[1245,1940,1941,1944,1946],{"class":1247,"line":340},[1245,1942,1943],{"class":1264},"reuse",[1245,1945,1433],{"class":1271},[1245,1947,1948],{"class":1257},"true\n",[1216,1950,1951],{},"代表離開這個 Route 時，希望先把它保存下來。",[1216,1953,1954],{},"接著建立自己的 Strategy。這裡先看整體結構，實際邏輯後面再一段一段拆開：",[1235,1956,1958],{"className":1237,"code":1957,"language":1239,"meta":1240,"style":1240},"import { Injectable } from '@angular\u002Fcore';\nimport {\n  ActivatedRouteSnapshot,\n  BaseRouteReuseStrategy,\n  DetachedRouteHandle,\n  Route,\n} from '@angular\u002Frouter';\n\n@Injectable()\nexport class CustomRouteReuseStrategy\n  extends BaseRouteReuseStrategy {\n\n  private readonly maxCacheSize = 10;\n\n  private readonly handles =\n    new Map\u003CRoute, DetachedRouteHandle>();\n\n  override shouldDetach(\n    route: ActivatedRouteSnapshot\n  ): boolean {\n    \u002F\u002F 判斷目前 Route 是否需要保存\n  }\n\n  override store(\n    route: ActivatedRouteSnapshot,\n    handle: DetachedRouteHandle | null\n  ): void {\n    \u002F\u002F 保存 detach 下來的 Route\n    \u002F\u002F 超過 Cache 上限時，銷毀並移除最早保存的 Route\n  }\n\n  override shouldAttach(\n    route: ActivatedRouteSnapshot\n  ): boolean {\n    \u002F\u002F 判斷是否有之前保存的 Route\n  }\n\n  override retrieve(\n    route: ActivatedRouteSnapshot\n  ): DetachedRouteHandle | null {\n    \u002F\u002F 取得之前保存的 Route\n  }\n}\n",[1242,1959,1960,1977,1984,1989,1994,1999,2004,2016,2022,2033,2043,2054,2058,2077,2081,2093,2115,2119,2130,2140,2152,2158,2163,2167,2176,2187,2203,2214,2219,2224,2228,2233,2243,2252,2263,2269,2274,2279,2289,2298,2314,2320,2325],{"__ignoreMap":1240},[1245,1961,1962,1965,1968,1971,1974],{"class":1247,"line":340},[1245,1963,1964],{"class":1250},"import",[1245,1966,1967],{"class":1271}," { Injectable } ",[1245,1969,1970],{"class":1250},"from",[1245,1972,1973],{"class":1285}," '@angular\u002Fcore'",[1245,1975,1976],{"class":1271},";\n",[1245,1978,1979,1981],{"class":1247,"line":329},[1245,1980,1964],{"class":1250},[1245,1982,1983],{"class":1271}," {\n",[1245,1985,1986],{"class":1247,"line":318},[1245,1987,1988],{"class":1271},"  ActivatedRouteSnapshot,\n",[1245,1990,1991],{"class":1247,"line":134},[1245,1992,1993],{"class":1271},"  BaseRouteReuseStrategy,\n",[1245,1995,1996],{"class":1247,"line":122},[1245,1997,1998],{"class":1271},"  DetachedRouteHandle,\n",[1245,2000,2001],{"class":1247,"line":31},[1245,2002,2003],{"class":1271},"  Route,\n",[1245,2005,2006,2009,2011,2014],{"class":1247,"line":17},[1245,2007,2008],{"class":1271},"} ",[1245,2010,1970],{"class":1250},[1245,2012,2013],{"class":1285}," '@angular\u002Frouter'",[1245,2015,1976],{"class":1271},[1245,2017,2018],{"class":1247,"line":110},[1245,2019,2021],{"emptyLinePlaceholder":2020},true,"\n",[1245,2023,2024,2027,2030],{"class":1247,"line":76},[1245,2025,2026],{"class":1271},"@",[1245,2028,2029],{"class":1609},"Injectable",[1245,2031,2032],{"class":1271},"()\n",[1245,2034,2035,2037,2040],{"class":1247,"line":247},[1245,2036,1251],{"class":1250},[1245,2038,2039],{"class":1250}," class",[1245,2041,2042],{"class":1264}," CustomRouteReuseStrategy\n",[1245,2044,2045,2048,2052],{"class":1247,"line":236},[1245,2046,2047],{"class":1250},"  extends",[1245,2049,2051],{"class":2050},"sMOND"," BaseRouteReuseStrategy",[1245,2053,1983],{"class":1271},[1245,2055,2056],{"class":1247,"line":225},[1245,2057,2021],{"emptyLinePlaceholder":2020},[1245,2059,2060,2063,2066,2070,2072,2075],{"class":1247,"line":214},[1245,2061,2062],{"class":1250},"  private",[1245,2064,2065],{"class":1250}," readonly",[1245,2067,2069],{"class":2068},"stnAF"," maxCacheSize",[1245,2071,1268],{"class":1250},[1245,2073,2074],{"class":1257}," 10",[1245,2076,1976],{"class":1271},[1245,2078,2079],{"class":1247,"line":203},[1245,2080,2021],{"emptyLinePlaceholder":2020},[1245,2082,2083,2085,2087,2090],{"class":1247,"line":192},[1245,2084,2062],{"class":1250},[1245,2086,2065],{"class":1250},[1245,2088,2089],{"class":2068}," handles",[1245,2091,2092],{"class":1250}," =\n",[1245,2094,2095,2098,2101,2103,2106,2109,2112],{"class":1247,"line":181},[1245,2096,2097],{"class":1250},"    new",[1245,2099,2100],{"class":1609}," Map",[1245,2102,1541],{"class":1271},[1245,2104,2105],{"class":1264},"Route",[1245,2107,2108],{"class":1271},", ",[1245,2110,2111],{"class":1264},"DetachedRouteHandle",[1245,2113,2114],{"class":1271},">();\n",[1245,2116,2117],{"class":1247,"line":170},[1245,2118,2021],{"emptyLinePlaceholder":2020},[1245,2120,2121,2124,2127],{"class":1247,"line":159},[1245,2122,2123],{"class":1250},"  override",[1245,2125,2126],{"class":1609}," shouldDetach",[1245,2128,2129],{"class":1271},"(\n",[1245,2131,2132,2135,2137],{"class":1247,"line":148},[1245,2133,2134],{"class":2068},"    route",[1245,2136,1261],{"class":1250},[1245,2138,2139],{"class":1264}," ActivatedRouteSnapshot\n",[1245,2141,2142,2145,2147,2150],{"class":1247,"line":137},[1245,2143,2144],{"class":1271},"  )",[1245,2146,1261],{"class":1250},[1245,2148,2149],{"class":1257}," boolean",[1245,2151,1983],{"class":1271},[1245,2153,2154],{"class":1247,"line":125},[1245,2155,2157],{"class":2156},"sDN9O","    \u002F\u002F 判斷目前 Route 是否需要保存\n",[1245,2159,2160],{"class":1247,"line":113},[1245,2161,2162],{"class":1271},"  }\n",[1245,2164,2165],{"class":1247,"line":101},[1245,2166,2021],{"emptyLinePlaceholder":2020},[1245,2168,2169,2171,2174],{"class":1247,"line":90},[1245,2170,2123],{"class":1250},[1245,2172,2173],{"class":1609}," store",[1245,2175,2129],{"class":1271},[1245,2177,2178,2180,2182,2185],{"class":1247,"line":79},[1245,2179,2134],{"class":2068},[1245,2181,1261],{"class":1250},[1245,2183,2184],{"class":1264}," ActivatedRouteSnapshot",[1245,2186,1289],{"class":1271},[1245,2188,2189,2192,2194,2197,2200],{"class":1247,"line":67},[1245,2190,2191],{"class":2068},"    handle",[1245,2193,1261],{"class":1250},[1245,2195,2196],{"class":1264}," DetachedRouteHandle",[1245,2198,2199],{"class":1250}," |",[1245,2201,2202],{"class":1257}," null\n",[1245,2204,2205,2207,2209,2212],{"class":1247,"line":56},[1245,2206,2144],{"class":1271},[1245,2208,1261],{"class":1250},[1245,2210,2211],{"class":1257}," void",[1245,2213,1983],{"class":1271},[1245,2215,2216],{"class":1247,"line":45},[1245,2217,2218],{"class":2156},"    \u002F\u002F 保存 detach 下來的 Route\n",[1245,2220,2221],{"class":1247,"line":34},[1245,2222,2223],{"class":2156},"    \u002F\u002F 超過 Cache 上限時，銷毀並移除最早保存的 Route\n",[1245,2225,2226],{"class":1247,"line":22},[1245,2227,2162],{"class":1271},[1245,2229,2231],{"class":1247,"line":2230},31,[1245,2232,2021],{"emptyLinePlaceholder":2020},[1245,2234,2236,2238,2241],{"class":1247,"line":2235},32,[1245,2237,2123],{"class":1250},[1245,2239,2240],{"class":1609}," shouldAttach",[1245,2242,2129],{"class":1271},[1245,2244,2246,2248,2250],{"class":1247,"line":2245},33,[1245,2247,2134],{"class":2068},[1245,2249,1261],{"class":1250},[1245,2251,2139],{"class":1264},[1245,2253,2255,2257,2259,2261],{"class":1247,"line":2254},34,[1245,2256,2144],{"class":1271},[1245,2258,1261],{"class":1250},[1245,2260,2149],{"class":1257},[1245,2262,1983],{"class":1271},[1245,2264,2266],{"class":1247,"line":2265},35,[1245,2267,2268],{"class":2156},"    \u002F\u002F 判斷是否有之前保存的 Route\n",[1245,2270,2272],{"class":1247,"line":2271},36,[1245,2273,2162],{"class":1271},[1245,2275,2277],{"class":1247,"line":2276},37,[1245,2278,2021],{"emptyLinePlaceholder":2020},[1245,2280,2282,2284,2287],{"class":1247,"line":2281},38,[1245,2283,2123],{"class":1250},[1245,2285,2286],{"class":1609}," retrieve",[1245,2288,2129],{"class":1271},[1245,2290,2292,2294,2296],{"class":1247,"line":2291},39,[1245,2293,2134],{"class":2068},[1245,2295,1261],{"class":1250},[1245,2297,2139],{"class":1264},[1245,2299,2301,2303,2305,2307,2309,2312],{"class":1247,"line":2300},40,[1245,2302,2144],{"class":1271},[1245,2304,1261],{"class":1250},[1245,2306,2196],{"class":1264},[1245,2308,2199],{"class":1250},[1245,2310,2311],{"class":1257}," null",[1245,2313,1983],{"class":1271},[1245,2315,2317],{"class":1247,"line":2316},41,[1245,2318,2319],{"class":2156},"    \u002F\u002F 取得之前保存的 Route\n",[1245,2321,2323],{"class":1247,"line":2322},42,[1245,2324,2162],{"class":1271},[1245,2326,2328],{"class":1247,"line":2327},43,[1245,2329,1451],{"class":1271},[1216,2331,2332,2333,2335,2336,2338],{},"這裡直接繼承 ",[1242,2334,1478],{},"，可以沿用 Angular 原本的 ",[1242,2337,1777],{}," 行為，不需要把預設規則重新寫一次。",[1403,2340,2342],{"id":2341},"離開-route-時怎麼保存","離開 Route 時怎麼保存？",[1216,2344,2345,2346,1643],{},"先看 ",[1242,2347,1737],{},[1235,2349,2351],{"className":1237,"code":2350,"language":1239,"meta":1240,"style":1240},"override shouldDetach(\n  route: ActivatedRouteSnapshot\n): boolean {\n  return route.data['reuse'] === true;\n}\n",[1242,2352,2353,2363,2368,2373,2394],{"__ignoreMap":1240},[1245,2354,2355,2358,2361],{"class":1247,"line":340},[1245,2356,2357],{"class":1271},"override ",[1245,2359,2360],{"class":1609},"shouldDetach",[1245,2362,2129],{"class":1271},[1245,2364,2365],{"class":1247,"line":329},[1245,2366,2367],{"class":1271},"  route: ActivatedRouteSnapshot\n",[1245,2369,2370],{"class":1247,"line":318},[1245,2371,2372],{"class":1271},"): boolean {\n",[1245,2374,2375,2378,2381,2384,2387,2389,2392],{"class":1247,"line":134},[1245,2376,2377],{"class":1250},"  return",[1245,2379,2380],{"class":1271}," route.data[",[1245,2382,2383],{"class":1285},"'reuse'",[1245,2385,2386],{"class":1271},"] ",[1245,2388,1492],{"class":1250},[1245,2390,2391],{"class":1257}," true",[1245,2393,1976],{"class":1271},[1245,2395,2396],{"class":1247,"line":122},[1245,2397,1451],{"class":1271},[1216,2399,2400,2401,2403],{},"當 Router 準備離開目前 Route 時，會透過 ",[1242,2402,1737],{}," 判斷這個頁面是否需要保存。",[1216,2405,2406],{},"Route 如果設定：",[1235,2408,2410],{"className":1237,"code":2409,"language":1239,"meta":1240,"style":1240},"data: {\n  reuse: true,\n}\n",[1242,2411,2412,2420,2431],{"__ignoreMap":1240},[1245,2413,2414,2417],{"class":1247,"line":340},[1245,2415,2416],{"class":1264},"data",[1245,2418,2419],{"class":1271},": {\n",[1245,2421,2422,2425,2427,2429],{"class":1247,"line":329},[1245,2423,2424],{"class":1264},"  reuse",[1245,2426,1433],{"class":1271},[1245,2428,1872],{"class":1257},[1245,2430,1289],{"class":1271},[1245,2432,2433],{"class":1247,"line":318},[1245,2434,1451],{"class":1271},[1216,2436,2437,2438,2440],{},"就會回傳 ",[1242,2439,1872],{},"，讓這次離開改用 detach，保留原本的 Component。",[1216,2442,2443,2444,1643],{},"detach 下來的內容接著會交給 ",[1242,2445,1747],{},[1235,2447,2449],{"className":1237,"code":2448,"language":1239,"meta":1240,"style":1240},"override store(\n  route: ActivatedRouteSnapshot,\n  handle: DetachedRouteHandle | null\n): void {\n  if (!handle || !route.routeConfig) {\n    return;\n  }\n    \n  this.handles.set(route.routeConfig, handle);\n}\n",[1242,2450,2451,2460,2465,2475,2485,2508,2515,2519,2524,2538],{"__ignoreMap":1240},[1245,2452,2453,2455,2458],{"class":1247,"line":340},[1245,2454,2357],{"class":1271},[1245,2456,2457],{"class":1609},"store",[1245,2459,2129],{"class":1271},[1245,2461,2462],{"class":1247,"line":329},[1245,2463,2464],{"class":1271},"  route: ActivatedRouteSnapshot,\n",[1245,2466,2467,2470,2473],{"class":1247,"line":318},[1245,2468,2469],{"class":1271},"  handle: DetachedRouteHandle ",[1245,2471,2472],{"class":1250},"|",[1245,2474,2202],{"class":1257},[1245,2476,2477,2480,2483],{"class":1247,"line":134},[1245,2478,2479],{"class":1271},"): ",[1245,2481,2482],{"class":1250},"void",[1245,2484,1983],{"class":1271},[1245,2486,2487,2490,2493,2496,2499,2502,2505],{"class":1247,"line":122},[1245,2488,2489],{"class":1250},"  if",[1245,2491,2492],{"class":1271}," (",[1245,2494,2495],{"class":1250},"!",[1245,2497,2498],{"class":1271},"handle ",[1245,2500,2501],{"class":1250},"||",[1245,2503,2504],{"class":1250}," !",[1245,2506,2507],{"class":1271},"route.routeConfig) {\n",[1245,2509,2510,2513],{"class":1247,"line":31},[1245,2511,2512],{"class":1250},"    return",[1245,2514,1976],{"class":1271},[1245,2516,2517],{"class":1247,"line":17},[1245,2518,2162],{"class":1271},[1245,2520,2521],{"class":1247,"line":110},[1245,2522,2523],{"class":1271},"    \n",[1245,2525,2526,2529,2532,2535],{"class":1247,"line":76},[1245,2527,2528],{"class":1257},"  this",[1245,2530,2531],{"class":1271},".handles.",[1245,2533,2534],{"class":1609},"set",[1245,2536,2537],{"class":1271},"(route.routeConfig, handle);\n",[1245,2539,2540],{"class":1247,"line":247},[1245,2541,1451],{"class":1271},[1216,2543,2544,2545,2548],{},"這裡使用 ",[1242,2546,2547],{},"Map"," 保存 Route：",[1235,2550,2552],{"className":1237,"code":2551,"language":1239,"meta":1240,"style":1240},"private readonly handles =\n  new Map\u003CRoute, DetachedRouteHandle>();\n",[1242,2553,2554,2562],{"__ignoreMap":1240},[1245,2555,2556,2559],{"class":1247,"line":340},[1245,2557,2558],{"class":1271},"private readonly handles ",[1245,2560,2561],{"class":1250},"=\n",[1245,2563,2564,2567,2569,2571,2573,2575,2577],{"class":1247,"line":329},[1245,2565,2566],{"class":1250},"  new",[1245,2568,2100],{"class":1609},[1245,2570,1541],{"class":1271},[1245,2572,2105],{"class":1264},[1245,2574,2108],{"class":1271},[1245,2576,2111],{"class":1264},[1245,2578,2114],{"class":1271},[1216,2580,2581,2584,2585,2588],{},[1242,2582,2583],{},"route.routeConfig"," 是 Key，用來辨識這是哪一個 Route；",[1242,2586,2587],{},"handle"," 則是 Value，保存這個 Route detach 下來的內容。",[1216,2590,2591],{},"所以：",[1235,2593,2595],{"className":1237,"code":2594,"language":1239,"meta":1240,"style":1240},"this.handles.set(route.routeConfig, handle);\n",[1242,2596,2597],{"__ignoreMap":1240},[1245,2598,2599,2601,2603,2605],{"class":1247,"line":340},[1245,2600,1128],{"class":1257},[1245,2602,2531],{"class":1271},[1245,2604,2534],{"class":1609},[1245,2606,2537],{"class":1271},[1216,2608,2609],{},"可以理解成把「這是哪個 Route」和「這個 Route 保存下來的內容」一起記錄起來。",[1403,2611,2613],{"id":2612},"最多只保留-10-個-route","最多只保留 10 個 Route",[1216,2615,2616],{},"Route 被 detach 之後，Component、View 和相關狀態都還存在。如果一直加入新的 Route，Cache 也會跟著增加。",[1216,2618,2619],{},"這裡把上限設成：",[1235,2621,2623],{"className":1237,"code":2622,"language":1239,"meta":1240,"style":1240},"private readonly maxCacheSize = 10;\n",[1242,2624,2625],{"__ignoreMap":1240},[1245,2626,2627,2630,2633,2635],{"class":1247,"line":340},[1245,2628,2629],{"class":1271},"private readonly maxCacheSize ",[1245,2631,2632],{"class":1250},"=",[1245,2634,2074],{"class":1257},[1245,2636,1976],{"class":1271},[1216,2638,2639],{},"當數量超過 10 個時，就移除最早加入的 Route：",[1235,2641,2643],{"className":1237,"code":2642,"language":1239,"meta":1240,"style":1240},"if (this.handles.size > this.maxCacheSize) {\n  const oldestRoute =\n    this.handles.keys().next().value;\n\n  if (oldestRoute) {\n    const oldestHandle =\n      this.handles.get(oldestRoute);\n\n    if (oldestHandle) {\n      destroyDetachedRouteHandle(oldestHandle);\n    }\n\n    this.handles.delete(oldestRoute);\n  }\n}\n",[1242,2644,2645,2666,2676,2695,2699,2706,2716,2729,2733,2741,2749,2754,2758,2769,2773],{"__ignoreMap":1240},[1245,2646,2647,2650,2652,2654,2657,2660,2663],{"class":1247,"line":340},[1245,2648,2649],{"class":1250},"if",[1245,2651,2492],{"class":1271},[1245,2653,1128],{"class":1257},[1245,2655,2656],{"class":1271},".handles.size ",[1245,2658,2659],{"class":1250},">",[1245,2661,2662],{"class":1257}," this",[1245,2664,2665],{"class":1271},".maxCacheSize) {\n",[1245,2667,2668,2671,2674],{"class":1247,"line":329},[1245,2669,2670],{"class":1250},"  const",[1245,2672,2673],{"class":1257}," oldestRoute",[1245,2675,2092],{"class":1250},[1245,2677,2678,2681,2683,2686,2689,2692],{"class":1247,"line":318},[1245,2679,2680],{"class":1257},"    this",[1245,2682,2531],{"class":1271},[1245,2684,2685],{"class":1609},"keys",[1245,2687,2688],{"class":1271},"().",[1245,2690,2691],{"class":1609},"next",[1245,2693,2694],{"class":1271},"().value;\n",[1245,2696,2697],{"class":1247,"line":134},[1245,2698,2021],{"emptyLinePlaceholder":2020},[1245,2700,2701,2703],{"class":1247,"line":122},[1245,2702,2489],{"class":1250},[1245,2704,2705],{"class":1271}," (oldestRoute) {\n",[1245,2707,2708,2711,2714],{"class":1247,"line":31},[1245,2709,2710],{"class":1250},"    const",[1245,2712,2713],{"class":1257}," oldestHandle",[1245,2715,2092],{"class":1250},[1245,2717,2718,2721,2723,2726],{"class":1247,"line":17},[1245,2719,2720],{"class":1257},"      this",[1245,2722,2531],{"class":1271},[1245,2724,2725],{"class":1609},"get",[1245,2727,2728],{"class":1271},"(oldestRoute);\n",[1245,2730,2731],{"class":1247,"line":110},[1245,2732,2021],{"emptyLinePlaceholder":2020},[1245,2734,2735,2738],{"class":1247,"line":76},[1245,2736,2737],{"class":1250},"    if",[1245,2739,2740],{"class":1271}," (oldestHandle) {\n",[1245,2742,2743,2746],{"class":1247,"line":247},[1245,2744,2745],{"class":1609},"      destroyDetachedRouteHandle",[1245,2747,2748],{"class":1271},"(oldestHandle);\n",[1245,2750,2751],{"class":1247,"line":236},[1245,2752,2753],{"class":1271},"    }\n",[1245,2755,2756],{"class":1247,"line":225},[1245,2757,2021],{"emptyLinePlaceholder":2020},[1245,2759,2760,2762,2764,2767],{"class":1247,"line":214},[1245,2761,2680],{"class":1257},[1245,2763,2531],{"class":1271},[1245,2765,2766],{"class":1609},"delete",[1245,2768,2728],{"class":1271},[1245,2770,2771],{"class":1247,"line":203},[1245,2772,2162],{"class":1271},[1245,2774,2775],{"class":1247,"line":192},[1245,2776,1451],{"class":1271},[1216,2778,2779,2780,2782],{},"JavaScript 的 ",[1242,2781,2547],{}," 會保留加入順序，所以：",[1235,2784,2786],{"className":1237,"code":2785,"language":1239,"meta":1240,"style":1240},"this.handles.keys().next().value\n",[1242,2787,2788],{"__ignoreMap":1240},[1245,2789,2790,2792,2794,2796,2798,2800],{"class":1247,"line":340},[1245,2791,1128],{"class":1257},[1245,2793,2531],{"class":1271},[1245,2795,2685],{"class":1609},[1245,2797,2688],{"class":1271},[1245,2799,2691],{"class":1609},[1245,2801,2802],{"class":1271},"().value\n",[1216,2804,2805],{},"可以取得最早加入的 Route。",[1216,2807,2808],{},"當第 11 個 Route 加入時，就會淘汰第 1 個，形成簡單的 FIFO（First In, First Out）策略。",[1216,2810,2811],{},"不過這裡不能只有：",[1235,2813,2815],{"className":1237,"code":2814,"language":1239,"meta":1240,"style":1240},"this.handles.delete(oldestRoute);\n",[1242,2816,2817],{"__ignoreMap":1240},[1245,2818,2819,2821,2823,2825],{"class":1247,"line":340},[1245,2820,1128],{"class":1257},[1245,2822,2531],{"class":1271},[1245,2824,2766],{"class":1609},[1245,2826,2728],{"class":1271},[1216,2828,2829,2830,2832],{},"因為這只是把 Handle 從 ",[1242,2831,2547],{}," 裡移除，被 detach 的 Component 本身還沒有被銷毀。",[1216,2834,2835,2836,2839],{},"Angular 21.1 開始提供 ",[1242,2837,2838],{},"destroyDetachedRouteHandle()","，可以在淘汰 Cache 時真正銷毀這個 Handle 對應的 Route 內容：",[1235,2841,2843],{"className":1237,"code":2842,"language":1239,"meta":1240,"style":1240},"destroyDetachedRouteHandle(oldestHandle);\nthis.handles.delete(oldestRoute);\n",[1242,2844,2845,2852],{"__ignoreMap":1240},[1245,2846,2847,2850],{"class":1247,"line":340},[1245,2848,2849],{"class":1609},"destroyDetachedRouteHandle",[1245,2851,2748],{"class":1271},[1245,2853,2854,2856,2858,2860],{"class":1247,"line":329},[1245,2855,1128],{"class":1257},[1245,2857,2531],{"class":1271},[1245,2859,2766],{"class":1609},[1245,2861,2728],{"class":1271},[1216,2863,2864],{},"這樣被淘汰的 Component 才會正式進入 Destroy，綁定在 Component 生命週期上的 Cleanup 也會一起執行。",[1403,2866,2868],{"id":2867},"再次進入時怎麼接回來","再次進入時怎麼接回來？",[1216,2870,2871,2872,2874,2875,2877],{},"重新進入 ",[1242,2873,1374],{}," 時，Router 會先透過 ",[1242,2876,1757],{}," 檢查之前是否保存過：",[1235,2879,2881],{"className":1237,"code":2880,"language":1239,"meta":1240,"style":1240},"override shouldAttach(\n  route: ActivatedRouteSnapshot\n): boolean {\n  if (!route.routeConfig) {\n    return false;\n  }\n\n  return this.handles.has(route.routeConfig);\n}\n",[1242,2882,2883,2892,2896,2900,2910,2919,2923,2927,2941],{"__ignoreMap":1240},[1245,2884,2885,2887,2890],{"class":1247,"line":340},[1245,2886,2357],{"class":1271},[1245,2888,2889],{"class":1609},"shouldAttach",[1245,2891,2129],{"class":1271},[1245,2893,2894],{"class":1247,"line":329},[1245,2895,2367],{"class":1271},[1245,2897,2898],{"class":1247,"line":318},[1245,2899,2372],{"class":1271},[1245,2901,2902,2904,2906,2908],{"class":1247,"line":134},[1245,2903,2489],{"class":1250},[1245,2905,2492],{"class":1271},[1245,2907,2495],{"class":1250},[1245,2909,2507],{"class":1271},[1245,2911,2912,2914,2917],{"class":1247,"line":122},[1245,2913,2512],{"class":1250},[1245,2915,2916],{"class":1257}," false",[1245,2918,1976],{"class":1271},[1245,2920,2921],{"class":1247,"line":31},[1245,2922,2162],{"class":1271},[1245,2924,2925],{"class":1247,"line":17},[1245,2926,2021],{"emptyLinePlaceholder":2020},[1245,2928,2929,2931,2933,2935,2938],{"class":1247,"line":110},[1245,2930,2377],{"class":1250},[1245,2932,2662],{"class":1257},[1245,2934,2531],{"class":1271},[1245,2936,2937],{"class":1609},"has",[1245,2939,2940],{"class":1271},"(route.routeConfig);\n",[1245,2942,2943],{"class":1247,"line":76},[1245,2944,1451],{"class":1271},[1216,2946,2947,2948,2950,2951,1382],{},"如果 ",[1242,2949,2547],{}," 裡有對應的 Route，就會回傳 ",[1242,2952,1872],{},[1216,2954,2955,2956,2958,2959,1643],{},"接著 ",[1242,2957,1767],{}," 會取回之前保存的 ",[1242,2960,2111],{},[1235,2962,2964],{"className":1237,"code":2963,"language":1239,"meta":1240,"style":1240},"override retrieve(\n  route: ActivatedRouteSnapshot\n): DetachedRouteHandle | null {\n  if (!route.routeConfig) {\n    return null;\n  }\n\n  return this.handles.get(route.routeConfig) ?? null;\n}\n",[1242,2965,2966,2975,2979,2990,3000,3008,3012,3016,3036],{"__ignoreMap":1240},[1245,2967,2968,2970,2973],{"class":1247,"line":340},[1245,2969,2357],{"class":1271},[1245,2971,2972],{"class":1609},"retrieve",[1245,2974,2129],{"class":1271},[1245,2976,2977],{"class":1247,"line":329},[1245,2978,2367],{"class":1271},[1245,2980,2981,2984,2986,2988],{"class":1247,"line":318},[1245,2982,2983],{"class":1271},"): DetachedRouteHandle ",[1245,2985,2472],{"class":1250},[1245,2987,2311],{"class":1257},[1245,2989,1983],{"class":1271},[1245,2991,2992,2994,2996,2998],{"class":1247,"line":134},[1245,2993,2489],{"class":1250},[1245,2995,2492],{"class":1271},[1245,2997,2495],{"class":1250},[1245,2999,2507],{"class":1271},[1245,3001,3002,3004,3006],{"class":1247,"line":122},[1245,3003,2512],{"class":1250},[1245,3005,2311],{"class":1257},[1245,3007,1976],{"class":1271},[1245,3009,3010],{"class":1247,"line":31},[1245,3011,2162],{"class":1271},[1245,3013,3014],{"class":1247,"line":17},[1245,3015,2021],{"emptyLinePlaceholder":2020},[1245,3017,3018,3020,3022,3024,3026,3029,3032,3034],{"class":1247,"line":110},[1245,3019,2377],{"class":1250},[1245,3021,2662],{"class":1257},[1245,3023,2531],{"class":1271},[1245,3025,2725],{"class":1609},[1245,3027,3028],{"class":1271},"(route.routeConfig) ",[1245,3030,3031],{"class":1250},"??",[1245,3033,2311],{"class":1257},[1245,3035,1976],{"class":1271},[1245,3037,3038],{"class":1247,"line":76},[1245,3039,1451],{"class":1271},[1216,3041,3042,3043,1382],{},"Router 取得 Handle 後，就能把原本保存的 Route attach 回 ",[1242,3044,1528],{},[1216,3046,3047,3048,3050,3051,3053],{},"因此重新進入 ",[1242,3049,1374],{}," 時，接回來的是原本的 ",[1242,3052,1378],{}," instance。",[1216,3055,3056],{},[1398,3057],{"alt":3058,"src":3059},"Route Reuse 完整流程","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355953\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Froute-reuse-full-flow.png",[1403,3061,3063],{"id":3062},"註冊-routereusestrategy","註冊 RouteReuseStrategy",[1216,3065,3066],{},"最後把自己的 Strategy 提供給 Angular：",[1235,3068,3070],{"className":1237,"code":3069,"language":1239,"meta":1240,"style":1240},"import {\n  provideRouter,\n  RouteReuseStrategy,\n} from '@angular\u002Frouter';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideRouter(routes),\n    {\n      provide: RouteReuseStrategy,\n      useClass: CustomRouteReuseStrategy,\n    },\n  ],\n};\n",[1242,3071,3072,3078,3083,3088,3098,3102,3120,3125,3133,3138,3143,3148,3152,3157],{"__ignoreMap":1240},[1245,3073,3074,3076],{"class":1247,"line":340},[1245,3075,1964],{"class":1250},[1245,3077,1983],{"class":1271},[1245,3079,3080],{"class":1247,"line":329},[1245,3081,3082],{"class":1271},"  provideRouter,\n",[1245,3084,3085],{"class":1247,"line":318},[1245,3086,3087],{"class":1271},"  RouteReuseStrategy,\n",[1245,3089,3090,3092,3094,3096],{"class":1247,"line":134},[1245,3091,2008],{"class":1271},[1245,3093,1970],{"class":1250},[1245,3095,2013],{"class":1285},[1245,3097,1976],{"class":1271},[1245,3099,3100],{"class":1247,"line":122},[1245,3101,2021],{"emptyLinePlaceholder":2020},[1245,3103,3104,3106,3108,3111,3113,3116,3118],{"class":1247,"line":31},[1245,3105,1251],{"class":1250},[1245,3107,1254],{"class":1250},[1245,3109,3110],{"class":1257}," appConfig",[1245,3112,1261],{"class":1250},[1245,3114,3115],{"class":1264}," ApplicationConfig",[1245,3117,1268],{"class":1250},[1245,3119,1983],{"class":1271},[1245,3121,3122],{"class":1247,"line":17},[1245,3123,3124],{"class":1271},"  providers: [\n",[1245,3126,3127,3130],{"class":1247,"line":110},[1245,3128,3129],{"class":1609},"    provideRouter",[1245,3131,3132],{"class":1271},"(routes),\n",[1245,3134,3135],{"class":1247,"line":76},[1245,3136,3137],{"class":1271},"    {\n",[1245,3139,3140],{"class":1247,"line":247},[1245,3141,3142],{"class":1271},"      provide: RouteReuseStrategy,\n",[1245,3144,3145],{"class":1247,"line":236},[1245,3146,3147],{"class":1271},"      useClass: CustomRouteReuseStrategy,\n",[1245,3149,3150],{"class":1247,"line":225},[1245,3151,1879],{"class":1271},[1245,3153,3154],{"class":1247,"line":214},[1245,3155,3156],{"class":1271},"  ],\n",[1245,3158,3159],{"class":1247,"line":203},[1245,3160,3161],{"class":1271},"};\n",[1216,3163,3164],{},"之後 Router 在處理 Navigation 時，就會使用這套規則。",[1403,3166,3168],{"id":3167},"實際看看-component-有沒有被-destroy","實際看看 Component 有沒有被 Destroy",[1216,3170,3171,3172,3174],{},"可以在 ",[1242,3173,1378],{}," 裡加入：",[1235,3176,3178],{"className":1237,"code":3177,"language":1239,"meta":1240,"style":1240},"import {\n  Component,\n  DestroyRef,\n  inject,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-patients',\n  template: `\n    \u003Cp>Patients\u003C\u002Fp>\n  `,\n})\nexport class PatientsComponent {\n  private destroyRef = inject(DestroyRef);\n\n  constructor() {\n    console.log('Patients created');\n\n    this.destroyRef.onDestroy(() => {\n      console.log('Patients destroyed');\n    });\n  }\n}\n",[1242,3179,3180,3186,3191,3196,3201,3211,3215,3225,3235,3243,3248,3255,3260,3271,3286,3290,3298,3315,3319,3337,3351,3356,3360],{"__ignoreMap":1240},[1245,3181,3182,3184],{"class":1247,"line":340},[1245,3183,1964],{"class":1250},[1245,3185,1983],{"class":1271},[1245,3187,3188],{"class":1247,"line":329},[1245,3189,3190],{"class":1271},"  Component,\n",[1245,3192,3193],{"class":1247,"line":318},[1245,3194,3195],{"class":1271},"  DestroyRef,\n",[1245,3197,3198],{"class":1247,"line":134},[1245,3199,3200],{"class":1271},"  inject,\n",[1245,3202,3203,3205,3207,3209],{"class":1247,"line":122},[1245,3204,2008],{"class":1271},[1245,3206,1970],{"class":1250},[1245,3208,1973],{"class":1285},[1245,3210,1976],{"class":1271},[1245,3212,3213],{"class":1247,"line":31},[1245,3214,2021],{"emptyLinePlaceholder":2020},[1245,3216,3217,3219,3222],{"class":1247,"line":17},[1245,3218,2026],{"class":1271},[1245,3220,3221],{"class":1609},"Component",[1245,3223,3224],{"class":1271},"({\n",[1245,3226,3227,3230,3233],{"class":1247,"line":110},[1245,3228,3229],{"class":1271},"  selector: ",[1245,3231,3232],{"class":1285},"'app-patients'",[1245,3234,1289],{"class":1271},[1245,3236,3237,3240],{"class":1247,"line":76},[1245,3238,3239],{"class":1271},"  template: ",[1245,3241,3242],{"class":1285},"`\n",[1245,3244,3245],{"class":1247,"line":247},[1245,3246,3247],{"class":1285},"    \u003Cp>Patients\u003C\u002Fp>\n",[1245,3249,3250,3253],{"class":1247,"line":236},[1245,3251,3252],{"class":1285},"  `",[1245,3254,1289],{"class":1271},[1245,3256,3257],{"class":1247,"line":225},[1245,3258,3259],{"class":1271},"})\n",[1245,3261,3262,3264,3266,3269],{"class":1247,"line":214},[1245,3263,1251],{"class":1250},[1245,3265,2039],{"class":1250},[1245,3267,3268],{"class":1264}," PatientsComponent",[1245,3270,1983],{"class":1271},[1245,3272,3273,3275,3278,3280,3283],{"class":1247,"line":203},[1245,3274,2062],{"class":1250},[1245,3276,3277],{"class":2068}," destroyRef",[1245,3279,1268],{"class":1250},[1245,3281,3282],{"class":1609}," inject",[1245,3284,3285],{"class":1271},"(DestroyRef);\n",[1245,3287,3288],{"class":1247,"line":192},[1245,3289,2021],{"emptyLinePlaceholder":2020},[1245,3291,3292,3295],{"class":1247,"line":181},[1245,3293,3294],{"class":1250},"  constructor",[1245,3296,3297],{"class":1271},"() {\n",[1245,3299,3300,3303,3306,3309,3312],{"class":1247,"line":170},[1245,3301,3302],{"class":1271},"    console.",[1245,3304,3305],{"class":1609},"log",[1245,3307,3308],{"class":1271},"(",[1245,3310,3311],{"class":1285},"'Patients created'",[1245,3313,3314],{"class":1271},");\n",[1245,3316,3317],{"class":1247,"line":159},[1245,3318,2021],{"emptyLinePlaceholder":2020},[1245,3320,3321,3323,3326,3329,3332,3335],{"class":1247,"line":148},[1245,3322,2680],{"class":1257},[1245,3324,3325],{"class":1271},".destroyRef.",[1245,3327,3328],{"class":1609},"onDestroy",[1245,3330,3331],{"class":1271},"(() ",[1245,3333,3334],{"class":1250},"=>",[1245,3336,1983],{"class":1271},[1245,3338,3339,3342,3344,3346,3349],{"class":1247,"line":137},[1245,3340,3341],{"class":1271},"      console.",[1245,3343,3305],{"class":1609},[1245,3345,3308],{"class":1271},[1245,3347,3348],{"class":1285},"'Patients destroyed'",[1245,3350,3314],{"class":1271},[1245,3352,3353],{"class":1247,"line":125},[1245,3354,3355],{"class":1271},"    });\n",[1245,3357,3358],{"class":1247,"line":113},[1245,3359,2162],{"class":1271},[1245,3361,3362],{"class":1247,"line":101},[1245,3363,1451],{"class":1271},[1216,3365,3366,3367,3369],{},"第一次進入 ",[1242,3368,1374],{}," 時會看到：",[1235,3371,3376],{"className":3372,"code":3374,"language":3375,"meta":1240},[3373],"language-text","Patients created\n","text",[1242,3377,3374],{"__ignoreMap":1240},[1216,3379,3380,3381,3383],{},"沒有使用 Route Reuse 時，離開 ",[1242,3382,1374],{}," 會銷毀 Component，因此會看到：",[1235,3385,3388],{"className":3386,"code":3387,"language":3375,"meta":1240},[3373],"Patients destroyed\n",[1242,3389,3387],{"__ignoreMap":1240},[1216,3391,3392,3393,3396,3397,3399,3400,1382],{},"設定 ",[1242,3394,3395],{},"reuse: true"," 後，離開 ",[1242,3398,1374],{}," 時 Component 會被 detach，這次切換不會觸發 ",[1242,3401,3402],{},"DestroyRef.onDestroy()",[1216,3404,3405,3406,3408,3409,3412],{},"再次切回 ",[1242,3407,1374],{}," 時，也不會重新出現 ",[1242,3410,3411],{},"Patients created","，因為 Router 接回的是原本保存的 Component。",[1216,3414,3415,3416,3418],{},"如果這個 Route 後來因為 FIFO 超過 Cache 上限而被淘汰，",[1242,3417,2838],{}," 會真正銷毀對應的 Component，這時才會看到：",[1235,3420,3422],{"className":3421,"code":3387,"language":3375,"meta":1240},[3373],[1242,3423,3387],{"__ignoreMap":1240},[1216,3425,3426,3428],{},[1242,3427,1528],{}," 本身也提供對應事件，可以直接觀察這些行為：",[1235,3430,3432],{"className":1532,"code":3431,"language":1534,"meta":1240,"style":1240},"\u003Crouter-outlet\n  (activate)=\"onActivate($event)\"\n  (deactivate)=\"onDeactivate($event)\"\n  (attach)=\"onAttach($event)\"\n  (detach)=\"onDetach($event)\"\n\u002F>\n",[1242,3433,3434,3441,3451,3461,3471,3481],{"__ignoreMap":1240},[1245,3435,3436,3438],{"class":1247,"line":340},[1245,3437,1541],{"class":1271},[1245,3439,3440],{"class":1544},"router-outlet\n",[1245,3442,3443,3446,3448],{"class":1247,"line":329},[1245,3444,3445],{"class":2050},"  (activate)",[1245,3447,2632],{"class":1271},[1245,3449,3450],{"class":1285},"\"onActivate($event)\"\n",[1245,3452,3453,3456,3458],{"class":1247,"line":318},[1245,3454,3455],{"class":2050},"  (deactivate)",[1245,3457,2632],{"class":1271},[1245,3459,3460],{"class":1285},"\"onDeactivate($event)\"\n",[1245,3462,3463,3466,3468],{"class":1247,"line":134},[1245,3464,3465],{"class":2050},"  (attach)",[1245,3467,2632],{"class":1271},[1245,3469,3470],{"class":1285},"\"onAttach($event)\"\n",[1245,3472,3473,3476,3478],{"class":1247,"line":122},[1245,3474,3475],{"class":2050},"  (detach)",[1245,3477,2632],{"class":1271},[1245,3479,3480],{"class":1285},"\"onDetach($event)\"\n",[1245,3482,3483],{"class":1247,"line":31},[1245,3484,3485],{"class":1271},"\u002F>\n",[1712,3487,3488,3498],{},[1715,3489,3490],{},[1718,3491,3492,3495],{},[1721,3493,3494],{},"Event",[1721,3496,3497],{},"發生時機",[1728,3499,3500,3510,3520,3529],{},[1718,3501,3502,3507],{},[1733,3503,3504],{},[1242,3505,3506],{},"activate",[1733,3508,3509],{},"建立新的 Route Component",[1718,3511,3512,3517],{},[1733,3513,3514],{},[1242,3515,3516],{},"deactivate",[1733,3518,3519],{},"Route Component 被銷毀",[1718,3521,3522,3526],{},[1733,3523,3524],{},[1242,3525,1662],{},[1733,3527,3528],{},"Route 被暫時取下並保存",[1718,3530,3531,3536],{},[1733,3532,3533],{},[1242,3534,3535],{},"attach",[1733,3537,3538],{},"保存的 Route 被重新接回",[1403,3540,3542],{"id":3541},"detach-不等於-destroy","detach 不等於 destroy",[1216,3544,3545],{},"如果 Component 裡有：",[1219,3547,3548,3551,3554,3557,3560,3565],{},[1222,3549,3550],{},"WebSocket",[1222,3552,3553],{},"SSE",[1222,3555,3556],{},"Timer",[1222,3558,3559],{},"Subscription",[1222,3561,3562],{},[1242,3563,3564],{},"effect()",[1222,3566,3567],{},"第三方 Library",[1216,3569,3570,3571,3574,3575,3577],{},"平常可能會在 ",[1242,3572,3573],{},"ngOnDestroy()"," 或 ",[1242,3576,3402],{}," 裡進行 Cleanup。",[1216,3579,3580],{},"detach 會保留 Component，因此這些 Cleanup 會等到 Component 真正被 Destroy 時才執行。",[1216,3582,3583,3585],{},[1242,3584,3564],{}," 也是一樣。如果 Effect 的生命週期和 Component 綁定，它會持續存在到 Component 被銷毀為止。",[1216,3587,3588],{},[1398,3589],{"alt":3542,"src":3590},"https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355959\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Fdetach-not-destroy.png",[1216,3592,3593],{},"所以畫面雖然已經切走，某些背景工作仍然可能繼續執行。",[1216,3595,3596],{},"這也是為什麼 Route Cache 需要清除策略。除了前面的 FIFO，也可以依照實際需求改成逾時清除、登出清除，或針對特定 Route 主動移除。",[1403,3598,3599],{"id":3599},"本日小結",[1216,3601,3602,3603,3605],{},"Angular Router 在切換不同 Route 時，原本的 Component 通常會被銷毀。透過 ",[1242,3604,1394],{},"，可以讓指定 Route 改用 detach，把原本的 Component instance 暫時保存下來。",[1216,3607,3608,3609,3611,3612,3614],{},"再次進入這個 Route 時，Router 會取回之前保存的 ",[1242,3610,2111],{},"，再 attach 回 ",[1242,3613,1528],{},"。因為接回的是同一個 Component，原本的搜尋條件、頁碼、表單內容和其他操作狀態都能繼續使用。",[1216,3616,3617,3618,3620,3621,3623],{},"Route Cache 也需要有上限。這篇利用 ",[1242,3619,2547],{}," 的插入順序做 FIFO，最多保留 10 個 Route，第 11 個加入時就淘汰最早保存的內容。Angular 21.1 起可以透過 ",[1242,3622,2838],{}," 把被淘汰的 Component 正確銷毀。",[1216,3625,3626],{},"detach 期間 Component 仍然活著，與生命週期綁定的 Effect、連線或其他背景工作也可能持續執行，因此保存 Route 時，也要留意這些資源是否仍有必要繼續存在。",[3628,3629,3630],"h3",{"id":3630},"資料來源",[1219,3632,3633,3646,3657,3676],{},[1222,3634,3635,3642,3643,3645],{},[3636,3637,3641],"a",{"href":3638,"rel":3639},"https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Frouter\u002Fsrc\u002Froute_reuse_strategy.ts",[3640],"nofollow","Angular 原始碼：route_reuse_strategy.ts","：五個方法的定義，以及 ",[1242,3644,1478],{}," 用 Route Config 判斷 reuse。",[1222,3647,3648,3653,3654,3656],{},[3636,3649,3652],{"href":3650,"rel":3651},"https:\u002F\u002Fangular.dev\u002Fguide\u002Frouting\u002Fcustomizing-route-behavior#manually-destroying-detached-route-handles",[3640],"Angular 官方文件：Manually destroying detached route handles","：用 ",[1242,3655,2838],{}," 銷毀不再保存的 Handle。",[1222,3658,3659,1643,3664,3666,3667,1511,3669,1511,3671,1511,3673,3675],{},[3636,3660,3663],{"href":3661,"rel":3662},"https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Frouter\u002Fsrc\u002Fdirectives\u002Frouter_outlet.ts",[3640],"Angular 原始碼：router_outlet.ts",[1242,3665,1528],{}," 的 ",[1242,3668,3506],{},[1242,3670,3516],{},[1242,3672,3535],{},[1242,3674,1662],{}," 事件。",[1222,3677,3678,1643,3683,3685,3686,3688,3689,1382],{},[3636,3679,3682],{"href":3680,"rel":3681},"https:\u002F\u002Fangular.dev\u002Fapi\u002Fcore\u002FViewContainerRef",[3640],"Angular 官方文件：ViewContainerRef",[1242,3684,1619],{}," 會銷毀 View，",[1242,3687,1642],{}," 取下後可再 ",[1242,3690,1670],{},[3692,3693,3694],"style",{},"html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sIAta, html code.shiki .sIAta{--shiki-default:#22863A;--shiki-dark:#8DDB8C}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .sMOND, html code.shiki .sMOND{--shiki-default:#6F42C1;--shiki-dark:#6CB6FF}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}",{"title":1240,"searchDepth":134,"depth":134,"links":3696},[3697,3698,3699,3700,3701,3702,3703,3704,3705,3706,3707,3708],{"id":1405,"depth":329,"text":1406},{"id":1521,"depth":329,"text":1522},{"id":1574,"depth":329,"text":1575},{"id":1704,"depth":329,"text":1705},{"id":1818,"depth":329,"text":1819},{"id":2341,"depth":329,"text":2342},{"id":2612,"depth":329,"text":2613},{"id":2867,"depth":329,"text":2868},{"id":3062,"depth":329,"text":3063},{"id":3167,"depth":329,"text":3168},{"id":3541,"depth":329,"text":3542},{"id":3599,"depth":329,"text":3599,"children":3709},[3710],{"id":3630,"depth":318,"text":3630},[11,13],"md",{},"---\ntitle: \"Day 23：RouteReuseStrategy，切換路由一定要銷毀 Component 嗎？\"\ndate: 2026-10-07 15:58:32\ntags:\n  - \"iThome 鐵人賽\"\n  - \"2026 鐵人賽\"\n  - \"Angular\"\ncategories:\n  - \"iThome 鐵人賽\"\n  - \"Angular\"\ndescription: \"在後台系統中，很常會看到頁籤式介面，例如：\"\ncanonical: https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422016\nseries:\n  name: \"2026 iThome 鐵人賽：Angular 22 Signal 進化論\"\n  url: https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F9939\n  day: 23\n---\n\n在後台系統中，很常會看到頁籤式介面，例如：\n\n- 病患列表\n- 掛號管理\n- 系統設定\n\n每個頁籤可能都對應不同的 Route：\n\n```ts\nexport const routes: Routes = [\n  {\n    path: 'patients',\n    component: PatientsComponent,\n  },\n  {\n    path: 'registrations',\n    component: RegistrationsComponent,\n  },\n  {\n    path: 'settings',\n    component: SettingsComponent,\n  },\n];\n```\n\n假設現在停在病患列表，而且已經操作了一段時間：\n\n- 搜尋：王小明\n- 狀態：看診中\n- 頁數：第 3 頁\n- Scroll 已經滑到中間\n\n接著切到「掛號管理」，之後再切回「病患列表」。\n\n一般情況下，離開 `\u002Fpatients` 時，`PatientsComponent` 會被銷毀；再次進入時，Angular 會建立新的 `PatientsComponent`。\n\n如果搜尋條件、頁碼或其他畫面狀態只存在 Component 裡，這些資料也會跟著消失。\n\n頁籤式操作通常希望使用者切到其他功能後，原本的頁面先保留下來，等切回來時可以接著操作。\n\n這時就可以使用 `RouteReuseStrategy`。\n\n![頁籤切換與畫面狀態保留](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355942\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Ftab-switch-state-preserved.png)\n\n## Angular 原本就有 Route Reuse\n\n在介紹自訂 `RouteReuseStrategy` 之前，要先知道 Angular Router 本來就有 Route Reuse 的概念。\n\n例如：\n\n```ts\n{\n  path: 'users\u002F:id',\n  component: UserComponent,\n}\n```\n\n當路由從 `\u002Fusers\u002F1` 切換到 `\u002Fusers\u002F2`，雖然 URL 不同，但兩邊使用的是同一份 Route Config。\n\nAngular 會繼續使用目前的 `UserComponent`，不會因為 `id` 改變就重新建立一個新的 Component。\n\n`BaseRouteReuseStrategy` 預設會判斷：\n\n```ts\nfuture.routeConfig === curr.routeConfig\n```\n\n只要前後使用的是同一份 Route Config，就可以直接 reuse。\n\n因此 Params、Query Params，甚至 URL `#` 後面的 Fragment 發生改變，都不代表 Component 一定會重新建立。\n\n這篇要處理的是另一種情況：在 `\u002Fpatients`、`\u002Fregistrations`、`\u002Fsettings` 這些不同 Route 之間切換時，希望其中某些頁面離開後仍然保留原本的 Component。\n\n## detach 和 destroy 的差別\n\nAngular Router 會透過 `RouterOutlet` 顯示目前 Route 對應的 Component：\n\n```html\n\u003Crouter-outlet \u002F>\n```\n\n一般離開 Route 時，目前的 Component 會被 deactivate 並銷毀。\n\n如果希望保留這個 Route，可以改成 detach。Component 會先從 `RouterOutlet` 中取下，原本的 instance 則繼續保留。\n\n之後再次進入這個 Route 時，再透過 attach 把保存的內容接回來。\n\n![一般切換 vs Route Reuse](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355945\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Fnormal-vs-route-reuse.png)\n\n`RouteReuseStrategy` 就是用來決定哪些 Route 離開時直接銷毀，哪些 Route 要先保存起來。\n\n## 這個概念和 View 的 detach 很像\n\nAngular 的 View API 也有類似設計。\n\n如果手上有一個 `ComponentRef`，可以先透過 `indexOf()` 找到這個 Component 對應的 View 在 Container 中的位置：\n\n```ts\nconst index = viewContainerRef.indexOf(componentRef.hostView);\n```\n\n使用 `remove()` 會把這個 View 從 Container 中移除並銷毀：\n\n```ts\nviewContainerRef.remove(index);\n```\n\n如果只想暫時把 View 取下來，同時保留 View 本身，可以改用 `detach()`：\n\n```ts\nconst view = viewContainerRef.detach(index);\n```\n\n之後還能透過 `insert()` 把同一個 View 放回去：\n\n```ts\nviewContainerRef.insert(view);\n```\n\n![Detach 概念對照：View 與 Route API](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791358207\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Fdetach-concept-comparison.png)\n\n兩邊不是同一套 API，但概念很接近：\n\n> 暫時從目前的位置移除，不代表一定要把它銷毀。\n\n## RouteReuseStrategy 負責什麼？\n\n`RouteReuseStrategy` 主要會處理五個方法：\n\n| 方法 | 用途 |\n| --- | --- |\n| `shouldDetach()` | 離開時判斷要不要保存 |\n| `store()` | 保存 detach 下來的 Route |\n| `shouldAttach()` | 進入時檢查是否有之前保存的 Route |\n| `retrieve()` | 取得之前保存的 Route |\n| `shouldReuseRoute()` | 判斷前後 Route 是否直接沿用 |\n\n![RouteReuseStrategy 五個方法](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355951\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Froute-reuse-strategy-methods.png)\n\n前四個方法可以分成兩組：\n\n- `shouldDetach()`、`store()`：離開 Route 時判斷是否保存，並把內容存起來\n- `shouldAttach()`、`retrieve()`：再次進入時檢查是否有保存內容，並把它取回來\n\n`shouldReuseRoute()`：負責判斷這次 Navigation 是否可以直接沿用目前的 Route。例如從 `\u002Fusers\u002F1` 切換到 `\u002Fusers\u002F2` 時，兩邊使用的是同一份 Route Config，因此可以直接 reuse。\n\n## 建立自己的 RouteReuseStrategy\n\n通常只需要保留特定頁面，可以先透過 Route Data 標記：\n\n```ts\nexport const routes: Routes = [\n  {\n    path: 'patients',\n    component: PatientsComponent,\n    data: {\n      reuse: true,\n    },\n  },\n  {\n    path: 'registrations',\n    component: RegistrationsComponent,\n  },\n  {\n    path: 'settings',\n    component: SettingsComponent,\n  },\n];\n```\n\n這裡只有 `\u002Fpatients` 設定：\n\n```ts\nreuse: true\n```\n\n代表離開這個 Route 時，希望先把它保存下來。\n\n接著建立自己的 Strategy。這裡先看整體結構，實際邏輯後面再一段一段拆開：\n\n```ts\nimport { Injectable } from '@angular\u002Fcore';\nimport {\n  ActivatedRouteSnapshot,\n  BaseRouteReuseStrategy,\n  DetachedRouteHandle,\n  Route,\n} from '@angular\u002Frouter';\n\n@Injectable()\nexport class CustomRouteReuseStrategy\n  extends BaseRouteReuseStrategy {\n\n  private readonly maxCacheSize = 10;\n\n  private readonly handles =\n    new Map\u003CRoute, DetachedRouteHandle>();\n\n  override shouldDetach(\n    route: ActivatedRouteSnapshot\n  ): boolean {\n    \u002F\u002F 判斷目前 Route 是否需要保存\n  }\n\n  override store(\n    route: ActivatedRouteSnapshot,\n    handle: DetachedRouteHandle | null\n  ): void {\n    \u002F\u002F 保存 detach 下來的 Route\n    \u002F\u002F 超過 Cache 上限時，銷毀並移除最早保存的 Route\n  }\n\n  override shouldAttach(\n    route: ActivatedRouteSnapshot\n  ): boolean {\n    \u002F\u002F 判斷是否有之前保存的 Route\n  }\n\n  override retrieve(\n    route: ActivatedRouteSnapshot\n  ): DetachedRouteHandle | null {\n    \u002F\u002F 取得之前保存的 Route\n  }\n}\n```\n\n這裡直接繼承 `BaseRouteReuseStrategy`，可以沿用 Angular 原本的 `shouldReuseRoute()` 行為，不需要把預設規則重新寫一次。\n\n## 離開 Route 時怎麼保存？\n\n先看 `shouldDetach()`：\n\n```ts\noverride shouldDetach(\n  route: ActivatedRouteSnapshot\n): boolean {\n  return route.data['reuse'] === true;\n}\n```\n\n當 Router 準備離開目前 Route 時，會透過 `shouldDetach()` 判斷這個頁面是否需要保存。\n\nRoute 如果設定：\n\n```ts\ndata: {\n  reuse: true,\n}\n```\n\n就會回傳 `true`，讓這次離開改用 detach，保留原本的 Component。\n\ndetach 下來的內容接著會交給 `store()`：\n\n```ts\noverride store(\n  route: ActivatedRouteSnapshot,\n  handle: DetachedRouteHandle | null\n): void {\n  if (!handle || !route.routeConfig) {\n    return;\n  }\n    \n  this.handles.set(route.routeConfig, handle);\n}\n```\n\n這裡使用 `Map` 保存 Route：\n\n```ts\nprivate readonly handles =\n  new Map\u003CRoute, DetachedRouteHandle>();\n```\n\n`route.routeConfig` 是 Key，用來辨識這是哪一個 Route；`handle` 則是 Value，保存這個 Route detach 下來的內容。\n\n所以：\n\n```ts\nthis.handles.set(route.routeConfig, handle);\n```\n\n可以理解成把「這是哪個 Route」和「這個 Route 保存下來的內容」一起記錄起來。\n\n## 最多只保留 10 個 Route\n\nRoute 被 detach 之後，Component、View 和相關狀態都還存在。如果一直加入新的 Route，Cache 也會跟著增加。\n\n這裡把上限設成：\n\n```ts\nprivate readonly maxCacheSize = 10;\n```\n\n當數量超過 10 個時，就移除最早加入的 Route：\n\n```ts\nif (this.handles.size > this.maxCacheSize) {\n  const oldestRoute =\n    this.handles.keys().next().value;\n\n  if (oldestRoute) {\n    const oldestHandle =\n      this.handles.get(oldestRoute);\n\n    if (oldestHandle) {\n      destroyDetachedRouteHandle(oldestHandle);\n    }\n\n    this.handles.delete(oldestRoute);\n  }\n}\n```\n\nJavaScript 的 `Map` 會保留加入順序，所以：\n\n```ts\nthis.handles.keys().next().value\n```\n\n可以取得最早加入的 Route。\n\n當第 11 個 Route 加入時，就會淘汰第 1 個，形成簡單的 FIFO（First In, First Out）策略。\n\n不過這裡不能只有：\n\n```ts\nthis.handles.delete(oldestRoute);\n```\n\n因為這只是把 Handle 從 `Map` 裡移除，被 detach 的 Component 本身還沒有被銷毀。\n\nAngular 21.1 開始提供 `destroyDetachedRouteHandle()`，可以在淘汰 Cache 時真正銷毀這個 Handle 對應的 Route 內容：\n\n```ts\ndestroyDetachedRouteHandle(oldestHandle);\nthis.handles.delete(oldestRoute);\n```\n\n這樣被淘汰的 Component 才會正式進入 Destroy，綁定在 Component 生命週期上的 Cleanup 也會一起執行。\n\n## 再次進入時怎麼接回來？\n\n重新進入 `\u002Fpatients` 時，Router 會先透過 `shouldAttach()` 檢查之前是否保存過：\n\n```ts\noverride shouldAttach(\n  route: ActivatedRouteSnapshot\n): boolean {\n  if (!route.routeConfig) {\n    return false;\n  }\n\n  return this.handles.has(route.routeConfig);\n}\n```\n\n如果 `Map` 裡有對應的 Route，就會回傳 `true`。\n\n接著 `retrieve()` 會取回之前保存的 `DetachedRouteHandle`：\n\n```ts\noverride retrieve(\n  route: ActivatedRouteSnapshot\n): DetachedRouteHandle | null {\n  if (!route.routeConfig) {\n    return null;\n  }\n\n  return this.handles.get(route.routeConfig) ?? null;\n}\n```\n\nRouter 取得 Handle 後，就能把原本保存的 Route attach 回 `RouterOutlet`。\n\n因此重新進入 `\u002Fpatients` 時，接回來的是原本的 `PatientsComponent` instance。\n\n![Route Reuse 完整流程](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355953\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Froute-reuse-full-flow.png)\n\n## 註冊 RouteReuseStrategy\n\n最後把自己的 Strategy 提供給 Angular：\n\n```ts\nimport {\n  provideRouter,\n  RouteReuseStrategy,\n} from '@angular\u002Frouter';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideRouter(routes),\n    {\n      provide: RouteReuseStrategy,\n      useClass: CustomRouteReuseStrategy,\n    },\n  ],\n};\n```\n\n之後 Router 在處理 Navigation 時，就會使用這套規則。\n\n## 實際看看 Component 有沒有被 Destroy\n\n可以在 `PatientsComponent` 裡加入：\n\n```ts\nimport {\n  Component,\n  DestroyRef,\n  inject,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-patients',\n  template: `\n    \u003Cp>Patients\u003C\u002Fp>\n  `,\n})\nexport class PatientsComponent {\n  private destroyRef = inject(DestroyRef);\n\n  constructor() {\n    console.log('Patients created');\n\n    this.destroyRef.onDestroy(() => {\n      console.log('Patients destroyed');\n    });\n  }\n}\n```\n\n第一次進入 `\u002Fpatients` 時會看到：\n\n```text\nPatients created\n```\n\n沒有使用 Route Reuse 時，離開 `\u002Fpatients` 會銷毀 Component，因此會看到：\n\n```text\nPatients destroyed\n```\n\n設定 `reuse: true` 後，離開 `\u002Fpatients` 時 Component 會被 detach，這次切換不會觸發 `DestroyRef.onDestroy()`。\n\n再次切回 `\u002Fpatients` 時，也不會重新出現 `Patients created`，因為 Router 接回的是原本保存的 Component。\n\n如果這個 Route 後來因為 FIFO 超過 Cache 上限而被淘汰，`destroyDetachedRouteHandle()` 會真正銷毀對應的 Component，這時才會看到：\n\n```text\nPatients destroyed\n```\n\n`RouterOutlet` 本身也提供對應事件，可以直接觀察這些行為：\n\n```html\n\u003Crouter-outlet\n  (activate)=\"onActivate($event)\"\n  (deactivate)=\"onDeactivate($event)\"\n  (attach)=\"onAttach($event)\"\n  (detach)=\"onDetach($event)\"\n\u002F>\n```\n\n| Event | 發生時機 |\n| --- | --- |\n| `activate` | 建立新的 Route Component |\n| `deactivate` | Route Component 被銷毀 |\n| `detach` | Route 被暫時取下並保存 |\n| `attach` | 保存的 Route 被重新接回 |\n\n## detach 不等於 destroy\n\n如果 Component 裡有：\n\n- WebSocket\n- SSE\n- Timer\n- Subscription\n- `effect()`\n- 第三方 Library\n\n平常可能會在 `ngOnDestroy()` 或 `DestroyRef.onDestroy()` 裡進行 Cleanup。\n\ndetach 會保留 Component，因此這些 Cleanup 會等到 Component 真正被 Destroy 時才執行。\n\n`effect()` 也是一樣。如果 Effect 的生命週期和 Component 綁定，它會持續存在到 Component 被銷毀為止。\n\n![detach 不等於 destroy](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791355959\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2023%EF%BC%9ARouteReuseStrategy%EF%BC%8C%E5%88%87%E6%8F%9B%E8%B7%AF%E7%94%B1%E4%B8%80%E5%AE%9A%E8%A6%81%E9%8A%B7%E6%AF%80%20Component%20%E5%97%8E%EF%BC%9F\u002Fdetach-not-destroy.png)\n\n所以畫面雖然已經切走，某些背景工作仍然可能繼續執行。\n\n這也是為什麼 Route Cache 需要清除策略。除了前面的 FIFO，也可以依照實際需求改成逾時清除、登出清除，或針對特定 Route 主動移除。\n\n## 本日小結\n\nAngular Router 在切換不同 Route 時，原本的 Component 通常會被銷毀。透過 `RouteReuseStrategy`，可以讓指定 Route 改用 detach，把原本的 Component instance 暫時保存下來。\n\n再次進入這個 Route 時，Router 會取回之前保存的 `DetachedRouteHandle`，再 attach 回 `RouterOutlet`。因為接回的是同一個 Component，原本的搜尋條件、頁碼、表單內容和其他操作狀態都能繼續使用。\n\nRoute Cache 也需要有上限。這篇利用 `Map` 的插入順序做 FIFO，最多保留 10 個 Route，第 11 個加入時就淘汰最早保存的內容。Angular 21.1 起可以透過 `destroyDetachedRouteHandle()` 把被淘汰的 Component 正確銷毀。\n\ndetach 期間 Component 仍然活著，與生命週期綁定的 Effect、連線或其他背景工作也可能持續執行，因此保存 Route 時，也要留意這些資源是否仍有必要繼續存在。\n\n### 資料來源\n\n- [Angular 原始碼：route\\_reuse\\_strategy.ts](https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Frouter\u002Fsrc\u002Froute_reuse_strategy.ts)：五個方法的定義，以及 `BaseRouteReuseStrategy` 用 Route Config 判斷 reuse。\n- [Angular 官方文件：Manually destroying detached route handles](https:\u002F\u002Fangular.dev\u002Fguide\u002Frouting\u002Fcustomizing-route-behavior#manually-destroying-detached-route-handles)：用 `destroyDetachedRouteHandle()` 銷毀不再保存的 Handle。\n- [Angular 原始碼：router\\_outlet.ts](https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Frouter\u002Fsrc\u002Fdirectives\u002Frouter_outlet.ts)：`RouterOutlet` 的 `activate`、`deactivate`、`attach`、`detach` 事件。\n- [Angular 官方文件：ViewContainerRef](https:\u002F\u002Fangular.dev\u002Fapi\u002Fcore\u002FViewContainerRef)：`remove()` 會銷毀 View，`detach()` 取下後可再 `insert()`。\n",{"title":93,"description":94},{"name":20,"url":21,"day":101},{"loc":92,"images":3718,"lastmod":95},[3719,3720,3721,3722,3723,3724],{"loc":1401},{"loc":1566},{"loc":1692},{"loc":1786},{"loc":3059},{"loc":3590},"ithome-2026-day-23","2026-10-07-ithome-2026-day-23",[11,12,13],"MYxA7Hu20W2eVA2dyMihdgcGRJ88NNP-0pghGAeU4EM",1792575173056]