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