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