[{"data":1,"prerenderedAt":3242},["ShallowReactive",2],{"all-posts":3,"post:\u002Fithome-2025-day-02":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":643,"body":1212,"canonical":649,"categories":3215,"date":645,"description":644,"draft":15,"extension":3216,"meta":3217,"navigation":1650,"ogImage":9,"path":642,"rawbody":3218,"readingTime":31,"robots":9,"schemaOrg":9,"seo":3219,"series":3220,"sitemap":3221,"slug":3238,"stem":3239,"tags":3240,"updated":9,"wordCount":648,"__hash__":3241},"posts\u002F2025-09-15-ithome-2025-day-02.md",{"type":1213,"value":1214,"toc":3192},"minimark",[1215,1223,1227,1229,1232,1242,1255,1260,1265,1271,1280,1286,1290,1295,1298,1309,1315,1320,1326,1329,1334,1337,1340,1343,1352,1358,1364,1387,1391,1396,1399,1412,1416,1424,1434,1438,1444,1457,1461,1467,1479,1491,1495,1500,1506,1510,1515,1521,1529,1539,1545,1556,1567,1571,1576,1592,1598,1603,1858,1862,1876,1881,2001,2005,2012,2017,2311,2315,2320,2330,2333,2339,2348,2357,2362,2366,2385,2393,2400,2404,2407,2409,2412,2418,2421,2425,2428,2448,2454,2464,2468,3172,3174,3177,3182,3188],[1216,1217,1218],"p",{},[1219,1220],"img",{"alt":1221,"src":1222},"2025 鐵人賽背景圖","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1758030949\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E5%BE%9E_Webhook_%E5%88%B0%E5%BE%8C%E7%AB%AF_%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA_refdrd.png",[1224,1225,1226],"h2",{"id":1226},"前言",[1216,1228,644],{},[1216,1230,1231],{},"本文將著重於實現以下核心功能：",[1233,1234,1235,1239],"ul",{},[1236,1237,1238],"li",{},"使用後端伺服器接收 LINE 伺服器推送的事件",[1236,1240,1241],{},"根據 LINE 伺服器訊息事件自動回覆使用者「Hello World」",[1243,1244,1245],"blockquote",{},[1216,1246,1247,1248],{},"本日程式碼的",[1249,1250,1254],"a",{"href":1251,"rel":1252},"https:\u002F\u002Fgithub.com\u002FLING-JUN-HAO\u002Fnestjs-linebot-ironman",[1253],"nofollow","範例連結",[1256,1257,1259],"h3",{"id":1258},"step-1啟用-line-official-account-webhook-功能","Step 1：啟用 LINE Official Account webhook 功能",[1243,1261,1262],{},[1216,1263,1264],{},"預設 webhook 功能是關閉，需要設置完 webhook URL 才可以啟用這個功能",[1216,1266,1267],{},[1219,1268],{"alt":1269,"src":1270},"LINE Official Account webhook 設定","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757928323\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_Official_Account_webhook_%E8%A8%AD%E5%AE%9A_vswo6w.jpg",[1243,1272,1273],{},[1216,1274,1275,1276],{},"這邊會讓你選擇 Provider，這邊我選擇使用 Day1 創建的",[1277,1278,1279],"code",{},"Antonio(2025 IT 鐵人賽專用)",[1216,1281,1282],{},[1219,1283],{"alt":1284,"src":1285},"LINE Official Account Messaging API Provider 選擇","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757928376\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_Official_Account_Messaging_API_Provider_%E9%81%B8%E6%93%87_hlh5cl.png",[1256,1287,1289],{"id":1288},"step-2設定你的-webhook-url-連結","Step 2：設定你的 webhook URL 連結",[1243,1291,1292],{},[1216,1293,1294],{},"Webhook URL 用於建立後端伺服器與 LINE 平台的連結關係",[1216,1296,1297],{},"完成 webhook Provider 設定後，如果回到 LINE Response Setting 頁面，你會發現原本說明已經改變，這表示已經進入到 webhook 設定的最後一個步驟：設置 webhook URL。",[1216,1299,1300,1301,1304,1305,1308],{},"接下來的所有設定操作都會改在 LINE Developers 平台進行。雖然 webhook URL 在",[1277,1302,1303],{},"LINE Developers"," 和 ",[1277,1306,1307],{},"LINE Official Account Manager"," 兩個平台都可以設定，但我們選擇在 LINE Developers 進行的主要原因是在 LINE developers 有驗證連線的按鈕可以確認連線是否成功。",[1216,1310,1311],{},[1219,1312],{"alt":1313,"src":1314},"LINE Official Account 啟用成功畫面","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1758022241\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_Official_Account_%E5%95%9F%E7%94%A8%E6%88%90%E5%8A%9F%E7%95%AB%E9%9D%A2%E9%A1%AF%E7%A4%BA_nf88mr.jpg",[1243,1316,1317],{},[1216,1318,1319],{},"LINE Official Account Response settings 說明改變",[1216,1321,1322],{},[1219,1323],{"alt":1324,"src":1325},"Line Official Account webhook 選項文字","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757929669\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLine_Official_Account_webhook_%E9%81%B8%E9%A0%85%E6%96%87%E5%AD%97_as14bh.png",[1327,1328],"hr",{},[1330,1331,1333],"h4",{"id":1332},"webhook-與-line-bot-之間的關係","Webhook 與 LINE Bot 之間的關係",[1216,1335,1336],{},"首先，這邊會帶著大家理解 LINE 平台與後端伺服器的關係，後端伺服器就像是一個 24hr 不休息的員工，我們可以讓他跟 LINE 平台連接，藉此達到即使不在電腦前面，當收到 LINE 官方帳號的相關事件(新朋友加入、封鎖、傳送訊息)伺服器也能代替我們處理。",[1216,1338,1339],{},"不僅如此，伺服器能夠按照我們所想的邏輯或是搭配 AI 回覆，讓回覆使用者的內容不要那麼像是罐頭訊息，也可以根據不同的使用情境(例如：聖誕節)動態調整回覆訊息的風格。",[1216,1341,1342],{},"以下是 LINE 平台跟後端伺服器搭配運作的關係圖：",[1243,1344,1345],{},[1216,1346,1347,1348,1351],{},"LINE 官方帳號訊息處理採用「事件驅動」的架構，透過 Webhook 機制實現即時的雙向溝通。",[1349,1350],"br",{},"\n(白話解釋： Webhook URL 就像是你的官方帳號頻道與 LINE 平台之間的專屬橋樑。當有使用者與官方帳號互動時，LINE 平台會主動透過這座橋樑將訊息推送到你的伺服器進行處理，而不需要你的伺服器不斷詢問「有沒有新訊息？」",[1216,1353,1354],{},[1219,1355],{"alt":1356,"src":1357},"LINE 平台與後端伺服器傳送訊息流程圖","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757930056\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLine_%E5%B9%B3%E5%8F%B0%E8%88%87%E5%BE%8C%E7%AB%AF%E4%BC%BA%E6%9C%8D%E5%99%A8%E5%82%B3%E9%80%81%E8%A8%8A%E6%81%AF%E6%B5%81%E7%A8%8B%E5%9C%96_akhtrf.png",[1216,1359,1360],{},[1361,1362,1363],"strong",{},"LINE 傳送訊息流程解釋：",[1243,1365,1366],{},[1367,1368,1369,1375,1381],"ol",{},[1236,1370,1371,1374],{},[1361,1372,1373],{},"用戶發送訊息","：使用者透過 LINE 應用程式發送一則訊息。",[1236,1376,1377,1380],{},[1361,1378,1379],{},"LINE 平台事件轉發","：當使用者與 LINE 官方帳號互動時，LINE 平台會根據綁定的 Channel ID(頻道識別碼)，將事件資料以 HTTP POST 方式傳送至頻道設定的 Webhook URL。伺服器接收到這些事件後，可以進行相對應的處理與回應。",[1236,1382,1383,1386],{},[1361,1384,1385],{},"伺服器回應處理","：當伺服器完成訊息處理後，透過 LINE Messaging API 將回應訊息傳送回 LINE 平台，最終傳達給使用者。",[1330,1388,1390],{"id":1389},"動手來做一個後端伺服器吧","動手來做一個後端伺服器吧！",[1243,1392,1393],{},[1216,1394,1395],{},"前期會搭配使用 ngrok 的方式，方便大家快速看到 Demo 成效",[1216,1397,1398],{},"首先，透過 NestJS CLI 快速建立後端的環境，讓大家快速體會使用 Webhook 與 LINE Bot 交互的感覺。",[1243,1400,1401,1404],{},[1216,1402,1403],{},"使用的環境配置如下：",[1233,1405,1406,1409],{},[1236,1407,1408],{},"nodejs 版本：v20.18.1",[1236,1410,1411],{},"nvm 版本：0.39.1",[1330,1413,1415],{"id":1414},"step-2-1輸入指令安裝-nestcli","Step 2-1：輸入指令安裝 NestCli",[1243,1417,1418],{},[1216,1419,1420,1421],{},"選擇你喜歡的套件管理器：如果不知道用哪一個推薦使用",[1277,1422,1423],{},"pnpm",[1425,1426,1431],"pre",{"className":1427,"code":1429,"language":1430},[1428],"language-text","pnpm add -g @nestjs\u002Fcli\n","text",[1277,1432,1429],{"__ignoreMap":1433},"",[1330,1435,1437],{"id":1436},"step-2-2輸入指令安裝-nestcli","Step 2-2：輸入指令安裝 NestCli",[1425,1439,1442],{"className":1440,"code":1441,"language":1430},[1428],"nest new linebot-webhook-server \u002F\u002F 創建一個 nest專案，名稱為 linebot-webhook-server\n",[1277,1443,1441],{"__ignoreMap":1433},[1216,1445,1446,1447,1450,1451,1453],{},"看到終端機寫著",[1277,1448,1449],{},"Successfully created","代表已經創立好專案囉！",[1349,1452],{},[1219,1454],{"alt":1455,"src":1456},"nest cli 完成畫面","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757941182\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002Fnest_cli_%E5%AE%8C%E6%88%90%E7%95%AB%E9%9D%A2_eqfsqp.png",[1330,1458,1460],{"id":1459},"step-2-3按照-nest-cli-的指示執行以下兩個指令","Step 2-3：按照 nest cli 的指示執行以下兩個指令",[1425,1462,1465],{"className":1463,"code":1464,"language":1430},[1428],"cd linebot-webhook-server \u002F\u002F 進入這個創建完的資料夾中\npnpm run start:dev \u002F\u002F 啟動這個 nest 專案(支援熱重載功能)\n",[1277,1466,1464],{"__ignoreMap":1433},[1216,1468,1446,1469,1472,1473,1475],{},[1277,1470,1471],{},"Nest application successfully started","代表伺服器成功啟動！",[1349,1474],{},[1219,1476],{"alt":1477,"src":1478},"開啟 NestJS 專案伺服器","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757941216\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E9%96%8B%E5%95%9F_NestJS_%E5%B0%88%E6%A1%88%E4%BC%BA%E6%9C%8D%E5%99%A8_vbmbya.png",[1243,1480,1481],{},[1216,1482,1483,1484,1488,1490],{},"預設伺服器啟動的網址是：",[1249,1485,1486],{"href":1486,"rel":1487},"http:\u002F\u002Flocalhost:3000",[1253],[1349,1489],{},"\n(看到 hello world 就代表順利透過後端伺服器取回預設回傳值)",[1330,1492,1494],{"id":1493},"step-2-4安裝-line-sdkline-bot-相關套件","Step 2-4：安裝 LINE sdk(LINE Bot 相關套件)",[1243,1496,1497],{},[1216,1498,1499],{},"記得先關掉伺服器(control + c)，再繼續安裝",[1425,1501,1504],{"className":1502,"code":1503,"language":1430},[1428],"pnpm install @line\u002Fbot-sdk --save\n",[1277,1505,1503],{"__ignoreMap":1433},[1330,1507,1509],{"id":1508},"step-2-5安裝處理環境變數的套件","Step 2-5：安裝處理環境變數的套件",[1243,1511,1512],{},[1216,1513,1514],{},"白話解釋：私人秘密不想被別人看到的檔案",[1425,1516,1519],{"className":1517,"code":1518,"language":1430},[1428],"pnpm install @nestjs\u002Fconfig\n",[1277,1520,1518],{"__ignoreMap":1433},[1330,1522,1524,1525,1528],{"id":1523},"step-2-6創建一個檔案命名env並且在其中貼上以下兩行","Step 2-6：創建一個檔案命名",[1277,1526,1527],{},".env","並且在其中貼上以下兩行",[1243,1530,1531],{},[1233,1532,1533,1536],{},[1236,1534,1535],{},"LINE_CHANNEL_SECRET：驗證請求後端伺服器來源的是 LINE 平台",[1236,1537,1538],{},"LINE_CHANNEL_ACCESS_TOKEN：驗證頻道授權者，可以透過 Bot 與使用者互動",[1425,1540,1543],{"className":1541,"code":1542,"language":1430},[1428],"LINE_CHANNEL_ACCESS_TOKEN=你的 LINE Bot 頻道存取權杖\nLINE_CHANNEL_SECRET=你的 LINE Bot 頻道密鑰\n",[1277,1544,1542],{"__ignoreMap":1433},[1216,1546,1547,1550,1552],{},[1361,1548,1549],{},"1️⃣ LINE_CHANNEL_SECRET 放在 Basic settings 頁籤：",[1349,1551],{},[1219,1553],{"alt":1554,"src":1555},"LINE_CHANNEL_SECRET","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757941436\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_CHANNEL_SECRET_ddedbd.png",[1216,1557,1558,1561,1563],{},[1361,1559,1560],{},"2️⃣ LINE_CHANNEL_ACCESS_TOKEN 放在 Messaging API 頁籤：",[1349,1562],{},[1219,1564],{"alt":1565,"src":1566},"LINE_CHANNEL_ACCESS_TOKEN","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757941478\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_CHANNEL_ACCESS_TOKEN_nialr0.png",[1330,1568,1570],{"id":1569},"step-2-7極簡化伺服器資料夾結構-註冊環境變數","Step 2-7：極簡化伺服器資料夾結構 + 註冊環境變數",[1243,1572,1573],{},[1216,1574,1575],{},"將檔案結構調整成最簡單能運行的方式，在逐漸優化！",[1233,1577,1578,1589],{},[1236,1579,1580,1581,1584,1585,1588],{},"刪除",[1277,1582,1583],{},"app.controller.spec","及",[1277,1586,1587],{},"app.service","的部分",[1236,1590,1591],{},"創建 config 資料夾及 line.config.ts 檔案",[1216,1593,1594],{},[1219,1595],{"alt":1596,"src":1597},"初始化檔案結構","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757930313\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FNestJS_%E5%88%9D%E5%A7%8B%E5%8C%96%E6%AA%94%E6%A1%88%E7%B5%90%E6%A7%8B_uhjjsk.png",[1216,1599,1600],{},[1361,1601,1602],{},"line.config.ts 內容",[1425,1604,1608],{"className":1605,"code":1606,"language":1607,"meta":1433,"style":1433},"language-ts shiki shiki-themes github-light github-dark-dimmed","import { ClientConfig } from '@line\u002Fbot-sdk';\nimport { ConfigService } from '@nestjs\u002Fconfig';\n\n\u002F\u002F 註冊名稱\nexport const LINE_CONFIG = 'LINE_CONFIG';\n\n\u002F\u002F 註冊的常數(這邊讀取的是.env 環境變數裡面的內容)\nconst lineConfig = (configService: ConfigService): ClientConfig => ({\n  channelAccessToken:\n    configService.get\u003Cstring>('LINE_CHANNEL_ACCESS_TOKEN') ?? '',\n  channelSecret: configService.get\u003Cstring>('LINE_CHANNEL_SECRET') ?? '',\n});\n\n\u002F\u002F 匯出成 NestJS Provider 供依賴注入系統使用(讓 NestJS 可以透過 DI 容器管理此設定)\nexport const LineConfigProvider = {\n  provide: LINE_CONFIG,\n  useFactory: (configService: ConfigService) => lineConfig(configService),\n  inject: [ConfigService],\n};\n","ts",[1277,1609,1610,1632,1646,1652,1658,1678,1682,1687,1725,1730,1762,1786,1791,1795,1800,1814,1824,1848,1853],{"__ignoreMap":1433},[1611,1612,1614,1618,1622,1625,1629],"span",{"class":1613,"line":340},"line",[1611,1615,1617],{"class":1616},"s7YZ4","import",[1611,1619,1621],{"class":1620},"sYgZi"," { ClientConfig } ",[1611,1623,1624],{"class":1616},"from",[1611,1626,1628],{"class":1627},"s-HuK"," '@line\u002Fbot-sdk'",[1611,1630,1631],{"class":1620},";\n",[1611,1633,1634,1636,1639,1641,1644],{"class":1613,"line":329},[1611,1635,1617],{"class":1616},[1611,1637,1638],{"class":1620}," { ConfigService } ",[1611,1640,1624],{"class":1616},[1611,1642,1643],{"class":1627}," '@nestjs\u002Fconfig'",[1611,1645,1631],{"class":1620},[1611,1647,1648],{"class":1613,"line":318},[1611,1649,1651],{"emptyLinePlaceholder":1650},true,"\n",[1611,1653,1654],{"class":1613,"line":134},[1611,1655,1657],{"class":1656},"sDN9O","\u002F\u002F 註冊名稱\n",[1611,1659,1660,1663,1666,1670,1673,1676],{"class":1613,"line":122},[1611,1661,1662],{"class":1616},"export",[1611,1664,1665],{"class":1616}," const",[1611,1667,1669],{"class":1668},"sviXB"," LINE_CONFIG",[1611,1671,1672],{"class":1616}," =",[1611,1674,1675],{"class":1627}," 'LINE_CONFIG'",[1611,1677,1631],{"class":1620},[1611,1679,1680],{"class":1613,"line":31},[1611,1681,1651],{"emptyLinePlaceholder":1650},[1611,1683,1684],{"class":1613,"line":17},[1611,1685,1686],{"class":1656},"\u002F\u002F 註冊的常數(這邊讀取的是.env 環境變數裡面的內容)\n",[1611,1688,1689,1691,1695,1697,1700,1704,1707,1711,1714,1716,1719,1722],{"class":1613,"line":110},[1611,1690,1029],{"class":1616},[1611,1692,1694],{"class":1693},"sPO5f"," lineConfig",[1611,1696,1672],{"class":1616},[1611,1698,1699],{"class":1620}," (",[1611,1701,1703],{"class":1702},"stnAF","configService",[1611,1705,1706],{"class":1616},":",[1611,1708,1710],{"class":1709},"sOLd2"," ConfigService",[1611,1712,1713],{"class":1620},")",[1611,1715,1706],{"class":1616},[1611,1717,1718],{"class":1709}," ClientConfig",[1611,1720,1721],{"class":1616}," =>",[1611,1723,1724],{"class":1620}," ({\n",[1611,1726,1727],{"class":1613,"line":76},[1611,1728,1729],{"class":1620},"  channelAccessToken:\n",[1611,1731,1732,1735,1738,1741,1744,1747,1750,1753,1756,1759],{"class":1613,"line":247},[1611,1733,1734],{"class":1620},"    configService.",[1611,1736,1737],{"class":1693},"get",[1611,1739,1740],{"class":1620},"\u003C",[1611,1742,1743],{"class":1668},"string",[1611,1745,1746],{"class":1620},">(",[1611,1748,1749],{"class":1627},"'LINE_CHANNEL_ACCESS_TOKEN'",[1611,1751,1752],{"class":1620},") ",[1611,1754,1755],{"class":1616},"??",[1611,1757,1758],{"class":1627}," ''",[1611,1760,1761],{"class":1620},",\n",[1611,1763,1764,1767,1769,1771,1773,1775,1778,1780,1782,1784],{"class":1613,"line":236},[1611,1765,1766],{"class":1620},"  channelSecret: configService.",[1611,1768,1737],{"class":1693},[1611,1770,1740],{"class":1620},[1611,1772,1743],{"class":1668},[1611,1774,1746],{"class":1620},[1611,1776,1777],{"class":1627},"'LINE_CHANNEL_SECRET'",[1611,1779,1752],{"class":1620},[1611,1781,1755],{"class":1616},[1611,1783,1758],{"class":1627},[1611,1785,1761],{"class":1620},[1611,1787,1788],{"class":1613,"line":225},[1611,1789,1790],{"class":1620},"});\n",[1611,1792,1793],{"class":1613,"line":214},[1611,1794,1651],{"emptyLinePlaceholder":1650},[1611,1796,1797],{"class":1613,"line":203},[1611,1798,1799],{"class":1656},"\u002F\u002F 匯出成 NestJS Provider 供依賴注入系統使用(讓 NestJS 可以透過 DI 容器管理此設定)\n",[1611,1801,1802,1804,1806,1809,1811],{"class":1613,"line":192},[1611,1803,1662],{"class":1616},[1611,1805,1665],{"class":1616},[1611,1807,1808],{"class":1668}," LineConfigProvider",[1611,1810,1672],{"class":1616},[1611,1812,1813],{"class":1620}," {\n",[1611,1815,1816,1819,1822],{"class":1613,"line":181},[1611,1817,1818],{"class":1620},"  provide: ",[1611,1820,1821],{"class":1668},"LINE_CONFIG",[1611,1823,1761],{"class":1620},[1611,1825,1826,1829,1832,1834,1836,1838,1840,1843,1845],{"class":1613,"line":170},[1611,1827,1828],{"class":1693},"  useFactory",[1611,1830,1831],{"class":1620},": (",[1611,1833,1703],{"class":1702},[1611,1835,1706],{"class":1616},[1611,1837,1710],{"class":1709},[1611,1839,1752],{"class":1620},[1611,1841,1842],{"class":1616},"=>",[1611,1844,1694],{"class":1693},[1611,1846,1847],{"class":1620},"(configService),\n",[1611,1849,1850],{"class":1613,"line":159},[1611,1851,1852],{"class":1620},"  inject: [ConfigService],\n",[1611,1854,1855],{"class":1613,"line":148},[1611,1856,1857],{"class":1620},"};\n",[1330,1859,1861],{"id":1860},"step-2-8將-line-config-provider-註冊到根模組中使用","Step 2-8：將 LINE Config Provider 註冊到根模組中使用",[1233,1863,1864,1870],{},[1236,1865,1866,1869],{},[1361,1867,1868],{},"ConfigModule.forRoot({ isGlobal: true })","：全域註冊環境變數，讓整個應用程式都能存取環境變數",[1236,1871,1872,1875],{},[1361,1873,1874],{},"LINE Config Provider 註冊","：將 LineConfigProvider 加入到 providers 陣列中，讓它可以被依賴注入系統使用。",[1216,1877,1878],{},[1361,1879,1880],{},"src\u002Fapp.module.ts：",[1425,1882,1884],{"className":1605,"code":1883,"language":1607,"meta":1433,"style":1433},"import { Module } from '@nestjs\u002Fcommon';\nimport { AppController } from '.\u002Fapp.controller';\nimport { ConfigModule } from '@nestjs\u002Fconfig';\nimport { LineConfigProvider } from '..\u002Fconfig\u002Fline.config';\n\n@Module({\n  imports: [ConfigModule.forRoot({ isGlobal: true })],\n  controllers: [AppController],\n  providers: [LineConfigProvider],\n})\nexport class AppModule {}\n",[1277,1885,1886,1900,1914,1927,1941,1945,1956,1973,1978,1983,1988],{"__ignoreMap":1433},[1611,1887,1888,1890,1893,1895,1898],{"class":1613,"line":340},[1611,1889,1617],{"class":1616},[1611,1891,1892],{"class":1620}," { Module } ",[1611,1894,1624],{"class":1616},[1611,1896,1897],{"class":1627}," '@nestjs\u002Fcommon'",[1611,1899,1631],{"class":1620},[1611,1901,1902,1904,1907,1909,1912],{"class":1613,"line":329},[1611,1903,1617],{"class":1616},[1611,1905,1906],{"class":1620}," { AppController } ",[1611,1908,1624],{"class":1616},[1611,1910,1911],{"class":1627}," '.\u002Fapp.controller'",[1611,1913,1631],{"class":1620},[1611,1915,1916,1918,1921,1923,1925],{"class":1613,"line":318},[1611,1917,1617],{"class":1616},[1611,1919,1920],{"class":1620}," { ConfigModule } ",[1611,1922,1624],{"class":1616},[1611,1924,1643],{"class":1627},[1611,1926,1631],{"class":1620},[1611,1928,1929,1931,1934,1936,1939],{"class":1613,"line":134},[1611,1930,1617],{"class":1616},[1611,1932,1933],{"class":1620}," { LineConfigProvider } ",[1611,1935,1624],{"class":1616},[1611,1937,1938],{"class":1627}," '..\u002Fconfig\u002Fline.config'",[1611,1940,1631],{"class":1620},[1611,1942,1943],{"class":1613,"line":122},[1611,1944,1651],{"emptyLinePlaceholder":1650},[1611,1946,1947,1950,1953],{"class":1613,"line":31},[1611,1948,1949],{"class":1620},"@",[1611,1951,1952],{"class":1693},"Module",[1611,1954,1955],{"class":1620},"({\n",[1611,1957,1958,1961,1964,1967,1970],{"class":1613,"line":17},[1611,1959,1960],{"class":1620},"  imports: [ConfigModule.",[1611,1962,1963],{"class":1693},"forRoot",[1611,1965,1966],{"class":1620},"({ isGlobal: ",[1611,1968,1969],{"class":1668},"true",[1611,1971,1972],{"class":1620}," })],\n",[1611,1974,1975],{"class":1613,"line":110},[1611,1976,1977],{"class":1620},"  controllers: [AppController],\n",[1611,1979,1980],{"class":1613,"line":76},[1611,1981,1982],{"class":1620},"  providers: [LineConfigProvider],\n",[1611,1984,1985],{"class":1613,"line":247},[1611,1986,1987],{"class":1620},"})\n",[1611,1989,1990,1992,1995,1998],{"class":1613,"line":236},[1611,1991,1662],{"class":1616},[1611,1993,1994],{"class":1616}," class",[1611,1996,1997],{"class":1709}," AppModule",[1611,1999,2000],{"class":1620}," {}\n",[1330,2002,2004],{"id":2003},"step-2-9設定-webhook-路徑接收-line-平台傳遞的事件","Step 2-9：設定 \u002Fwebhook 路徑接收 LINE 平台傳遞的事件",[1216,2006,2007,2008,2011],{},"這裡設定的路由前綴為 ",[1277,2009,2010],{},"\u002Fwebhook","，這是專門用來接收 LINE Bot webhook 事件的端點路徑。當 LINE 平台有訊息或事件需要推送給我們的應用程式時，會透過這個特定的路由路徑進行回調通知",[1216,2013,2014],{},[1361,2015,2016],{},"src\u002Fapp.controller.ts：",[1425,2018,2020],{"className":1605,"code":2019,"language":1607,"meta":1433,"style":1433},"import { Body, Controller, Inject, Post } from '@nestjs\u002Fcommon';\nimport {\n  ClientConfig,\n  WebhookRequestBody,\n  messagingApi,\n  MessageEvent,\n  WebhookEvent,\n} from '@line\u002Fbot-sdk';\nimport { LINE_CONFIG } from '..\u002Fconfig\u002Fline.config';\n\n@Controller()\nexport class AppController {\n  private readonly lineClient: messagingApi.MessagingApiClient;\n\n  \u002F\u002F 根據配置檔案初始化 LINE Messaging API 客戶端\n  constructor(@Inject(LINE_CONFIG) private readonly lineConfig: ClientConfig) {\n    this.lineClient = new messagingApi.MessagingApiClient({\n      channelAccessToken: this.lineConfig.channelAccessToken,\n    });\n  }\n\n  @Post('\u002Fwebhook')\n  handleWebhook(@Body() body: WebhookRequestBody): Promise\u003Cstring> {\n    console.log('Webhook 從 line platform 接收到的資訊:', body);\n    return 'Webhook processed successfully';\n  }\n}\n",[1277,2021,2022,2035,2041,2046,2051,2056,2061,2066,2077,2090,2094,2104,2115,2139,2143,2148,2180,2201,2211,2216,2221,2225,2241,2276,2292,2302,2306],{"__ignoreMap":1433},[1611,2023,2024,2026,2029,2031,2033],{"class":1613,"line":340},[1611,2025,1617],{"class":1616},[1611,2027,2028],{"class":1620}," { Body, Controller, Inject, Post } ",[1611,2030,1624],{"class":1616},[1611,2032,1897],{"class":1627},[1611,2034,1631],{"class":1620},[1611,2036,2037,2039],{"class":1613,"line":329},[1611,2038,1617],{"class":1616},[1611,2040,1813],{"class":1620},[1611,2042,2043],{"class":1613,"line":318},[1611,2044,2045],{"class":1620},"  ClientConfig,\n",[1611,2047,2048],{"class":1613,"line":134},[1611,2049,2050],{"class":1620},"  WebhookRequestBody,\n",[1611,2052,2053],{"class":1613,"line":122},[1611,2054,2055],{"class":1620},"  messagingApi,\n",[1611,2057,2058],{"class":1613,"line":31},[1611,2059,2060],{"class":1620},"  MessageEvent,\n",[1611,2062,2063],{"class":1613,"line":17},[1611,2064,2065],{"class":1620},"  WebhookEvent,\n",[1611,2067,2068,2071,2073,2075],{"class":1613,"line":110},[1611,2069,2070],{"class":1620},"} ",[1611,2072,1624],{"class":1616},[1611,2074,1628],{"class":1627},[1611,2076,1631],{"class":1620},[1611,2078,2079,2081,2084,2086,2088],{"class":1613,"line":76},[1611,2080,1617],{"class":1616},[1611,2082,2083],{"class":1620}," { LINE_CONFIG } ",[1611,2085,1624],{"class":1616},[1611,2087,1938],{"class":1627},[1611,2089,1631],{"class":1620},[1611,2091,2092],{"class":1613,"line":247},[1611,2093,1651],{"emptyLinePlaceholder":1650},[1611,2095,2096,2098,2101],{"class":1613,"line":236},[1611,2097,1949],{"class":1620},[1611,2099,2100],{"class":1693},"Controller",[1611,2102,2103],{"class":1620},"()\n",[1611,2105,2106,2108,2110,2113],{"class":1613,"line":225},[1611,2107,1662],{"class":1616},[1611,2109,1994],{"class":1616},[1611,2111,2112],{"class":1709}," AppController",[1611,2114,1813],{"class":1620},[1611,2116,2117,2120,2123,2126,2128,2131,2134,2137],{"class":1613,"line":214},[1611,2118,2119],{"class":1616},"  private",[1611,2121,2122],{"class":1616}," readonly",[1611,2124,2125],{"class":1702}," lineClient",[1611,2127,1706],{"class":1616},[1611,2129,2130],{"class":1709}," messagingApi",[1611,2132,2133],{"class":1620},".",[1611,2135,2136],{"class":1709},"MessagingApiClient",[1611,2138,1631],{"class":1620},[1611,2140,2141],{"class":1613,"line":203},[1611,2142,1651],{"emptyLinePlaceholder":1650},[1611,2144,2145],{"class":1613,"line":192},[1611,2146,2147],{"class":1656},"  \u002F\u002F 根據配置檔案初始化 LINE Messaging API 客戶端\n",[1611,2149,2150,2153,2156,2159,2162,2164,2166,2169,2171,2173,2175,2177],{"class":1613,"line":181},[1611,2151,2152],{"class":1616},"  constructor",[1611,2154,2155],{"class":1620},"(@",[1611,2157,2158],{"class":1693},"Inject",[1611,2160,2161],{"class":1620},"(",[1611,2163,1821],{"class":1668},[1611,2165,1752],{"class":1620},[1611,2167,2168],{"class":1616},"private",[1611,2170,2122],{"class":1616},[1611,2172,1694],{"class":1702},[1611,2174,1706],{"class":1616},[1611,2176,1718],{"class":1709},[1611,2178,2179],{"class":1620},") {\n",[1611,2181,2182,2185,2188,2191,2194,2197,2199],{"class":1613,"line":170},[1611,2183,2184],{"class":1668},"    this",[1611,2186,2187],{"class":1620},".lineClient ",[1611,2189,2190],{"class":1616},"=",[1611,2192,2193],{"class":1616}," new",[1611,2195,2196],{"class":1620}," messagingApi.",[1611,2198,2136],{"class":1693},[1611,2200,1955],{"class":1620},[1611,2202,2203,2206,2208],{"class":1613,"line":159},[1611,2204,2205],{"class":1620},"      channelAccessToken: ",[1611,2207,1128],{"class":1668},[1611,2209,2210],{"class":1620},".lineConfig.channelAccessToken,\n",[1611,2212,2213],{"class":1613,"line":148},[1611,2214,2215],{"class":1620},"    });\n",[1611,2217,2218],{"class":1613,"line":137},[1611,2219,2220],{"class":1620},"  }\n",[1611,2222,2223],{"class":1613,"line":125},[1611,2224,1651],{"emptyLinePlaceholder":1650},[1611,2226,2227,2230,2233,2235,2238],{"class":1613,"line":113},[1611,2228,2229],{"class":1620},"  @",[1611,2231,2232],{"class":1693},"Post",[1611,2234,2161],{"class":1620},[1611,2236,2237],{"class":1627},"'\u002Fwebhook'",[1611,2239,2240],{"class":1620},")\n",[1611,2242,2243,2246,2248,2251,2254,2257,2259,2262,2264,2266,2269,2271,2273],{"class":1613,"line":101},[1611,2244,2245],{"class":1693},"  handleWebhook",[1611,2247,2155],{"class":1620},[1611,2249,2250],{"class":1693},"Body",[1611,2252,2253],{"class":1620},"() ",[1611,2255,2256],{"class":1702},"body",[1611,2258,1706],{"class":1616},[1611,2260,2261],{"class":1709}," WebhookRequestBody",[1611,2263,1713],{"class":1620},[1611,2265,1706],{"class":1616},[1611,2267,2268],{"class":1709}," Promise",[1611,2270,1740],{"class":1620},[1611,2272,1743],{"class":1668},[1611,2274,2275],{"class":1620},"> {\n",[1611,2277,2278,2281,2284,2286,2289],{"class":1613,"line":90},[1611,2279,2280],{"class":1620},"    console.",[1611,2282,2283],{"class":1693},"log",[1611,2285,2161],{"class":1620},[1611,2287,2288],{"class":1627},"'Webhook 從 line platform 接收到的資訊:'",[1611,2290,2291],{"class":1620},", body);\n",[1611,2293,2294,2297,2300],{"class":1613,"line":79},[1611,2295,2296],{"class":1616},"    return",[1611,2298,2299],{"class":1627}," 'Webhook processed successfully'",[1611,2301,1631],{"class":1620},[1611,2303,2304],{"class":1613,"line":67},[1611,2305,2220],{"class":1620},[1611,2307,2308],{"class":1613,"line":56},[1611,2309,2310],{"class":1620},"}\n",[1330,2312,2314],{"id":2313},"step-2-10nest-後端伺服器開啟的狀態下搭配使用-ngrok","Step 2-10：Nest 後端伺服器開啟的狀態下搭配使用 ngrok",[1243,2316,2317],{},[1216,2318,2319],{},"LINE Webhook URL 必須是 HTTPS 網址，所以這邊需要搭配 ngrok 使用",[1216,2321,2322,2325,2326,2329],{},[1277,2323,2324],{},"ngrok","是一個反向代理工具，能夠將本地端口轉發至公開的",[1277,2327,2328],{},"HTTPS 網址","。它提供第三方代理服務，讓外部用戶可以直接透過公開網址存取您本地運行的應用程式",[1216,2331,2332],{},"使用指令非常簡單，僅需要打開終端機輸入以下指令：",[1425,2334,2337],{"className":2335,"code":2336,"language":1430},[1428],"ngrok http 3000\n",[1277,2338,2336],{"__ignoreMap":1433},[1216,2340,2341,2342,2344],{},"看到以下顯示的樣子就代表已經成功建立公開網址",[1349,2343],{},[1219,2345],{"alt":2346,"src":2347},"ngrok 運行畫面","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757940102\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002Fngrok_%E9%81%8B%E8%A1%8C%E7%95%AB%E9%9D%A2_cmndnz.png",[1216,2349,2350],{},[1361,2351,2352,2353,2356],{},"這個",[1277,2354,2355],{},"https:\u002F\u002Fd120-49-213-168-27.ngrok-free.app","就是 webhook URL",[1243,2358,2359],{},[1216,2360,2361],{},"這種方式屬於臨時伺服器的方式，連結有時候會斷開。如果重新開啟 ngrok 需要記得 https 連結位置會改變，不要忘記連帶改動設定的 Webhook URL",[1256,2363,2365],{"id":2364},"step-3在-line-developers-設置-webhook-url-並測試","Step 3：在 LINE developers 設置 webhook URL 並測試",[1216,2367,2368,2369,2374,2375,2378,2379,2381],{},"登入 ",[1249,2370,2373],{"href":2371,"rel":2372},"https:\u002F\u002Fdevelopers.line.biz\u002Fzh-hant\u002F",[1253],"LINE developers"," 後，在 ",[1277,2376,2377],{},"Message API Webhook settings","可以設定 Webhook URL：",[1349,2380],{},[1219,2382],{"alt":2383,"src":2384},"在 LINE developers 設置 Webhook URL","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757940044\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E5%9C%A8_Line_developers_%E8%A8%AD%E7%BD%AE_Webhook_URL_gvhmnd.png",[1243,2386,2387],{},[1216,2388,2389,2390,2392],{},"特別注意要加上 ",[1277,2391,2010],{}," 的路徑前綴，這是我們後端設置接收 LINE 事件的端點，這樣才可以正確打到架設的後端伺服器喔！",[1216,2394,2395,2396,2399],{},"按下",[1277,2397,2398],{},"Verify 按鈕","的同時，LINE 平台就會發送事件到你的後端伺服器，這時候只要後端伺服器回傳的請求狀態碼是 200，畫面上會顯示 Success。這時候就代表已經成功建立 LINE 平台與你架設的後端伺服器之間的連結。",[1256,2401,2403],{"id":2402},"step-4在-line-developers-打開-use-webhook-按鈕","Step 4：在 LINE developers 打開 Use webhook 按鈕",[1216,2405,2406],{},"在剛才設置 Webhook URL 的下方，找到並開啟「Use webhook」開關。此設定會與先前在 Line Official Account Manager 中的配置進行連動。",[1327,2408],{},[1216,2410,2411],{},"開啟後，你可以試著在官方帳號輸入訊息，查看終端機印出的訊息：",[1216,2413,2414],{},[1219,2415],{"alt":2416,"src":2417},"後端伺服器終端機印出 LINE 平台送出的事件","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757940193\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E5%BE%8C%E7%AB%AF%E4%BC%BA%E6%9C%8D%E5%99%A8%E7%B5%82%E7%AB%AF%E6%A9%9F%E5%8D%B0%E5%87%BA_Line_%E5%B9%B3%E5%8F%B0%E9%80%81%E5%87%BA%E7%9A%84%E4%BA%8B%E4%BB%B6_cqcwpa.png",[1216,2419,2420],{},"印出收到的訊息，代表你已經能成功接收到使用者跟你的 LINE 官方帳號互動的事件囉！",[1256,2422,2424],{"id":2423},"step-5修改-appcontrollerts-回覆使用者-hello-world","Step 5：修改 app.controller.ts 回覆使用者 Hello World！",[1216,2426,2427],{},"LINE Bot 與使用者的互動包含多種事件類型，這邊舉例三種最常見的事件：",[1233,2429,2430,2436,2442],{},[1236,2431,2432,2435],{},[1361,2433,2434],{},"Follow event","：用戶加入 LINE 官方帳號",[1236,2437,2438,2441],{},[1361,2439,2440],{},"Unfollow event","：用戶封鎖 LINE 官方帳號",[1236,2443,2444,2447],{},[1361,2445,2446],{},"Message event","：用戶傳送訊息",[1216,2449,2450,2451,2453],{},"本系列的文章將聚焦於最常見的使用情境 ",[1277,2452,2446],{},"。",[1216,2455,2456,2457,2460,2461,2453],{},"在下面程式碼中，我們會特別展示一個重要概念：每則訊息都帶有專屬的 ",[1361,2458,2459],{},"Reply Token","（可以理解為該訊息的「身分證」）。後端伺服器可以利用這個 Token 來回應特定的用戶訊息，這就是 LINE Bot 回覆訊息的第一種方式 ",[1361,2462,2463],{},"Reply Message",[1216,2465,2466],{},[1361,2467,2016],{},[1425,2469,2471],{"className":1605,"code":2470,"language":1607,"meta":1433,"style":1433},"\u002F\u002F...略\n\n\u002F\u002F 限制型別先收斂成部分事件，搭配 eventHandler 使用\ntype HandlerMap = {\n  [K in WebhookEvent['type']]: (\n    event: Extract\u003CWebhookEvent, { type: K }>,\n  ) => Promise\u003Cvoid>;\n};\n\n@Controller()\nexport class AppController {\n  private readonly lineClient: messagingApi.MessagingApiClient;\n\n  \u002F\u002F 根據配置檔案初始化 LINE Messaging API 客戶端\n  constructor(@Inject(LINE_CONFIG) private readonly lineConfig: ClientConfig) {\n    this.lineClient = new messagingApi.MessagingApiClient({\n      channelAccessToken: this.lineConfig.channelAccessToken,\n    });\n  }\n\n  \u002F**\n   * Webhook 端點處理器\n   * 接收來自 LINE Platform 的事件通知\n   * @param body LINE Platform 傳送的 Webhook 請求本體\n   * @returns 處理完成的回應訊息\n   *\u002F\n  @Post('\u002Fwebhook')\n  async handleWebhook(@Body() body: WebhookRequestBody): Promise\u003Cstring> {\n    console.log('Webhook 從 LINE 平台接收到的資訊:', body);\n    const { events } = body;\n\n    \u002F\u002F 事件處理器映射表\n    const eventHandler = {\n      message: (event) => this.handleMessageEvent(event),\n    } satisfies Partial\u003CHandlerMap>;\n\n    \u002F\u002F 逐項處理每一個事件\n    for (const event of events) {\n      const { type } = event;\n      if (type === 'message') {\n        await eventHandler[type](event);\n      }\n    }\n\n    return 'Webhook processed successfully';\n  }\n\n  \u002F**\n   * 處理訊息事件\n   * @param event 訊息事件物件，包含用戶訊息內容(text)和回覆憑證(replyToken)\n   *\u002F\n  private async handleMessageEvent(event: MessageEvent): Promise\u003Cvoid> {\n    const { replyToken } = event;\n    console.log('收到訊息事件', event);\n    console.log('訊息憑證(身分證):', replyToken);\n    await this.lineClient.replyMessage({\n      replyToken,\n      messages: [\n        {\n          type: 'text',\n          text: 'hello world',\n        },\n      ],\n    });\n  }\n}\n",[1277,2472,2473,2478,2482,2487,2499,2527,2555,2572,2576,2580,2588,2598,2616,2620,2624,2650,2666,2674,2678,2682,2686,2691,2696,2701,2715,2725,2730,2742,2774,2787,2806,2811,2817,2829,2855,2874,2879,2885,2904,2921,2938,2947,2953,2959,2964,2973,2978,2983,2988,2994,3006,3011,3043,3059,3074,3089,3105,3111,3117,3123,3134,3145,3151,3157,3162,3167],{"__ignoreMap":1433},[1611,2474,2475],{"class":1613,"line":340},[1611,2476,2477],{"class":1656},"\u002F\u002F...略\n",[1611,2479,2480],{"class":1613,"line":329},[1611,2481,1651],{"emptyLinePlaceholder":1650},[1611,2483,2484],{"class":1613,"line":318},[1611,2485,2486],{"class":1656},"\u002F\u002F 限制型別先收斂成部分事件，搭配 eventHandler 使用\n",[1611,2488,2489,2492,2495,2497],{"class":1613,"line":134},[1611,2490,2491],{"class":1616},"type",[1611,2493,2494],{"class":1709}," HandlerMap",[1611,2496,1672],{"class":1616},[1611,2498,1813],{"class":1620},[1611,2500,2501,2504,2507,2510,2513,2516,2519,2522,2524],{"class":1613,"line":122},[1611,2502,2503],{"class":1620},"  [",[1611,2505,2506],{"class":1709},"K",[1611,2508,2509],{"class":1616}," in",[1611,2511,2512],{"class":1709}," WebhookEvent",[1611,2514,2515],{"class":1620},"[",[1611,2517,2518],{"class":1627},"'type'",[1611,2520,2521],{"class":1620},"]]",[1611,2523,1706],{"class":1616},[1611,2525,2526],{"class":1620}," (\n",[1611,2528,2529,2532,2534,2537,2539,2542,2545,2547,2549,2552],{"class":1613,"line":31},[1611,2530,2531],{"class":1702},"    event",[1611,2533,1706],{"class":1616},[1611,2535,2536],{"class":1709}," Extract",[1611,2538,1740],{"class":1620},[1611,2540,2541],{"class":1709},"WebhookEvent",[1611,2543,2544],{"class":1620},", { ",[1611,2546,2491],{"class":1702},[1611,2548,1706],{"class":1616},[1611,2550,2551],{"class":1709}," K",[1611,2553,2554],{"class":1620}," }>,\n",[1611,2556,2557,2560,2562,2564,2566,2569],{"class":1613,"line":17},[1611,2558,2559],{"class":1620},"  ) ",[1611,2561,1842],{"class":1616},[1611,2563,2268],{"class":1709},[1611,2565,1740],{"class":1620},[1611,2567,2568],{"class":1668},"void",[1611,2570,2571],{"class":1620},">;\n",[1611,2573,2574],{"class":1613,"line":110},[1611,2575,1857],{"class":1620},[1611,2577,2578],{"class":1613,"line":76},[1611,2579,1651],{"emptyLinePlaceholder":1650},[1611,2581,2582,2584,2586],{"class":1613,"line":247},[1611,2583,1949],{"class":1620},[1611,2585,2100],{"class":1693},[1611,2587,2103],{"class":1620},[1611,2589,2590,2592,2594,2596],{"class":1613,"line":236},[1611,2591,1662],{"class":1616},[1611,2593,1994],{"class":1616},[1611,2595,2112],{"class":1709},[1611,2597,1813],{"class":1620},[1611,2599,2600,2602,2604,2606,2608,2610,2612,2614],{"class":1613,"line":225},[1611,2601,2119],{"class":1616},[1611,2603,2122],{"class":1616},[1611,2605,2125],{"class":1702},[1611,2607,1706],{"class":1616},[1611,2609,2130],{"class":1709},[1611,2611,2133],{"class":1620},[1611,2613,2136],{"class":1709},[1611,2615,1631],{"class":1620},[1611,2617,2618],{"class":1613,"line":214},[1611,2619,1651],{"emptyLinePlaceholder":1650},[1611,2621,2622],{"class":1613,"line":203},[1611,2623,2147],{"class":1656},[1611,2625,2626,2628,2630,2632,2634,2636,2638,2640,2642,2644,2646,2648],{"class":1613,"line":192},[1611,2627,2152],{"class":1616},[1611,2629,2155],{"class":1620},[1611,2631,2158],{"class":1693},[1611,2633,2161],{"class":1620},[1611,2635,1821],{"class":1668},[1611,2637,1752],{"class":1620},[1611,2639,2168],{"class":1616},[1611,2641,2122],{"class":1616},[1611,2643,1694],{"class":1702},[1611,2645,1706],{"class":1616},[1611,2647,1718],{"class":1709},[1611,2649,2179],{"class":1620},[1611,2651,2652,2654,2656,2658,2660,2662,2664],{"class":1613,"line":181},[1611,2653,2184],{"class":1668},[1611,2655,2187],{"class":1620},[1611,2657,2190],{"class":1616},[1611,2659,2193],{"class":1616},[1611,2661,2196],{"class":1620},[1611,2663,2136],{"class":1693},[1611,2665,1955],{"class":1620},[1611,2667,2668,2670,2672],{"class":1613,"line":170},[1611,2669,2205],{"class":1620},[1611,2671,1128],{"class":1668},[1611,2673,2210],{"class":1620},[1611,2675,2676],{"class":1613,"line":159},[1611,2677,2215],{"class":1620},[1611,2679,2680],{"class":1613,"line":148},[1611,2681,2220],{"class":1620},[1611,2683,2684],{"class":1613,"line":137},[1611,2685,1651],{"emptyLinePlaceholder":1650},[1611,2687,2688],{"class":1613,"line":125},[1611,2689,2690],{"class":1656},"  \u002F**\n",[1611,2692,2693],{"class":1613,"line":113},[1611,2694,2695],{"class":1656},"   * Webhook 端點處理器\n",[1611,2697,2698],{"class":1613,"line":101},[1611,2699,2700],{"class":1656},"   * 接收來自 LINE Platform 的事件通知\n",[1611,2702,2703,2706,2709,2712],{"class":1613,"line":90},[1611,2704,2705],{"class":1656},"   * ",[1611,2707,2708],{"class":1616},"@param",[1611,2710,2711],{"class":1620}," body",[1611,2713,2714],{"class":1656}," LINE Platform 傳送的 Webhook 請求本體\n",[1611,2716,2717,2719,2722],{"class":1613,"line":79},[1611,2718,2705],{"class":1656},[1611,2720,2721],{"class":1616},"@returns",[1611,2723,2724],{"class":1656}," 處理完成的回應訊息\n",[1611,2726,2727],{"class":1613,"line":67},[1611,2728,2729],{"class":1656},"   *\u002F\n",[1611,2731,2732,2734,2736,2738,2740],{"class":1613,"line":56},[1611,2733,2229],{"class":1620},[1611,2735,2232],{"class":1693},[1611,2737,2161],{"class":1620},[1611,2739,2237],{"class":1627},[1611,2741,2240],{"class":1620},[1611,2743,2744,2747,2750,2752,2754,2756,2758,2760,2762,2764,2766,2768,2770,2772],{"class":1613,"line":45},[1611,2745,2746],{"class":1616},"  async",[1611,2748,2749],{"class":1693}," handleWebhook",[1611,2751,2155],{"class":1620},[1611,2753,2250],{"class":1693},[1611,2755,2253],{"class":1620},[1611,2757,2256],{"class":1702},[1611,2759,1706],{"class":1616},[1611,2761,2261],{"class":1709},[1611,2763,1713],{"class":1620},[1611,2765,1706],{"class":1616},[1611,2767,2268],{"class":1709},[1611,2769,1740],{"class":1620},[1611,2771,1743],{"class":1668},[1611,2773,2275],{"class":1620},[1611,2775,2776,2778,2780,2782,2785],{"class":1613,"line":34},[1611,2777,2280],{"class":1620},[1611,2779,2283],{"class":1693},[1611,2781,2161],{"class":1620},[1611,2783,2784],{"class":1627},"'Webhook 從 LINE 平台接收到的資訊:'",[1611,2786,2291],{"class":1620},[1611,2788,2789,2792,2795,2798,2801,2803],{"class":1613,"line":22},[1611,2790,2791],{"class":1616},"    const",[1611,2793,2794],{"class":1620}," { ",[1611,2796,2797],{"class":1668},"events",[1611,2799,2800],{"class":1620}," } ",[1611,2802,2190],{"class":1616},[1611,2804,2805],{"class":1620}," body;\n",[1611,2807,2809],{"class":1613,"line":2808},31,[1611,2810,1651],{"emptyLinePlaceholder":1650},[1611,2812,2814],{"class":1613,"line":2813},32,[1611,2815,2816],{"class":1656},"    \u002F\u002F 事件處理器映射表\n",[1611,2818,2820,2822,2825,2827],{"class":1613,"line":2819},33,[1611,2821,2791],{"class":1616},[1611,2823,2824],{"class":1668}," eventHandler",[1611,2826,1672],{"class":1616},[1611,2828,1813],{"class":1620},[1611,2830,2832,2835,2837,2840,2842,2844,2847,2849,2852],{"class":1613,"line":2831},34,[1611,2833,2834],{"class":1693},"      message",[1611,2836,1831],{"class":1620},[1611,2838,2839],{"class":1702},"event",[1611,2841,1752],{"class":1620},[1611,2843,1842],{"class":1616},[1611,2845,2846],{"class":1668}," this",[1611,2848,2133],{"class":1620},[1611,2850,2851],{"class":1693},"handleMessageEvent",[1611,2853,2854],{"class":1620},"(event),\n",[1611,2856,2858,2861,2864,2867,2869,2872],{"class":1613,"line":2857},35,[1611,2859,2860],{"class":1620},"    } ",[1611,2862,2863],{"class":1616},"satisfies",[1611,2865,2866],{"class":1709}," Partial",[1611,2868,1740],{"class":1620},[1611,2870,2871],{"class":1709},"HandlerMap",[1611,2873,2571],{"class":1620},[1611,2875,2877],{"class":1613,"line":2876},36,[1611,2878,1651],{"emptyLinePlaceholder":1650},[1611,2880,2882],{"class":1613,"line":2881},37,[1611,2883,2884],{"class":1656},"    \u002F\u002F 逐項處理每一個事件\n",[1611,2886,2888,2891,2893,2895,2898,2901],{"class":1613,"line":2887},38,[1611,2889,2890],{"class":1616},"    for",[1611,2892,1699],{"class":1620},[1611,2894,1029],{"class":1616},[1611,2896,2897],{"class":1668}," event",[1611,2899,2900],{"class":1616}," of",[1611,2902,2903],{"class":1620}," events) {\n",[1611,2905,2907,2910,2912,2914,2916,2918],{"class":1613,"line":2906},39,[1611,2908,2909],{"class":1616},"      const",[1611,2911,2794],{"class":1620},[1611,2913,2491],{"class":1668},[1611,2915,2800],{"class":1620},[1611,2917,2190],{"class":1616},[1611,2919,2920],{"class":1620}," event;\n",[1611,2922,2924,2927,2930,2933,2936],{"class":1613,"line":2923},40,[1611,2925,2926],{"class":1616},"      if",[1611,2928,2929],{"class":1620}," (type ",[1611,2931,2932],{"class":1616},"===",[1611,2934,2935],{"class":1627}," 'message'",[1611,2937,2179],{"class":1620},[1611,2939,2941,2944],{"class":1613,"line":2940},41,[1611,2942,2943],{"class":1616},"        await",[1611,2945,2946],{"class":1620}," eventHandler[type](event);\n",[1611,2948,2950],{"class":1613,"line":2949},42,[1611,2951,2952],{"class":1620},"      }\n",[1611,2954,2956],{"class":1613,"line":2955},43,[1611,2957,2958],{"class":1620},"    }\n",[1611,2960,2962],{"class":1613,"line":2961},44,[1611,2963,1651],{"emptyLinePlaceholder":1650},[1611,2965,2967,2969,2971],{"class":1613,"line":2966},45,[1611,2968,2296],{"class":1616},[1611,2970,2299],{"class":1627},[1611,2972,1631],{"class":1620},[1611,2974,2976],{"class":1613,"line":2975},46,[1611,2977,2220],{"class":1620},[1611,2979,2981],{"class":1613,"line":2980},47,[1611,2982,1651],{"emptyLinePlaceholder":1650},[1611,2984,2986],{"class":1613,"line":2985},48,[1611,2987,2690],{"class":1656},[1611,2989,2991],{"class":1613,"line":2990},49,[1611,2992,2993],{"class":1656},"   * 處理訊息事件\n",[1611,2995,2997,2999,3001,3003],{"class":1613,"line":2996},50,[1611,2998,2705],{"class":1656},[1611,3000,2708],{"class":1616},[1611,3002,2897],{"class":1620},[1611,3004,3005],{"class":1656}," 訊息事件物件，包含用戶訊息內容(text)和回覆憑證(replyToken)\n",[1611,3007,3009],{"class":1613,"line":3008},51,[1611,3010,2729],{"class":1656},[1611,3012,3014,3016,3019,3022,3024,3026,3028,3031,3033,3035,3037,3039,3041],{"class":1613,"line":3013},52,[1611,3015,2119],{"class":1616},[1611,3017,3018],{"class":1616}," async",[1611,3020,3021],{"class":1693}," handleMessageEvent",[1611,3023,2161],{"class":1620},[1611,3025,2839],{"class":1702},[1611,3027,1706],{"class":1616},[1611,3029,3030],{"class":1709}," MessageEvent",[1611,3032,1713],{"class":1620},[1611,3034,1706],{"class":1616},[1611,3036,2268],{"class":1709},[1611,3038,1740],{"class":1620},[1611,3040,2568],{"class":1668},[1611,3042,2275],{"class":1620},[1611,3044,3046,3048,3050,3053,3055,3057],{"class":1613,"line":3045},53,[1611,3047,2791],{"class":1616},[1611,3049,2794],{"class":1620},[1611,3051,3052],{"class":1668},"replyToken",[1611,3054,2800],{"class":1620},[1611,3056,2190],{"class":1616},[1611,3058,2920],{"class":1620},[1611,3060,3062,3064,3066,3068,3071],{"class":1613,"line":3061},54,[1611,3063,2280],{"class":1620},[1611,3065,2283],{"class":1693},[1611,3067,2161],{"class":1620},[1611,3069,3070],{"class":1627},"'收到訊息事件'",[1611,3072,3073],{"class":1620},", event);\n",[1611,3075,3077,3079,3081,3083,3086],{"class":1613,"line":3076},55,[1611,3078,2280],{"class":1620},[1611,3080,2283],{"class":1693},[1611,3082,2161],{"class":1620},[1611,3084,3085],{"class":1627},"'訊息憑證(身分證):'",[1611,3087,3088],{"class":1620},", replyToken);\n",[1611,3090,3092,3095,3097,3100,3103],{"class":1613,"line":3091},56,[1611,3093,3094],{"class":1616},"    await",[1611,3096,2846],{"class":1668},[1611,3098,3099],{"class":1620},".lineClient.",[1611,3101,3102],{"class":1693},"replyMessage",[1611,3104,1955],{"class":1620},[1611,3106,3108],{"class":1613,"line":3107},57,[1611,3109,3110],{"class":1620},"      replyToken,\n",[1611,3112,3114],{"class":1613,"line":3113},58,[1611,3115,3116],{"class":1620},"      messages: [\n",[1611,3118,3120],{"class":1613,"line":3119},59,[1611,3121,3122],{"class":1620},"        {\n",[1611,3124,3126,3129,3132],{"class":1613,"line":3125},60,[1611,3127,3128],{"class":1620},"          type: ",[1611,3130,3131],{"class":1627},"'text'",[1611,3133,1761],{"class":1620},[1611,3135,3137,3140,3143],{"class":1613,"line":3136},61,[1611,3138,3139],{"class":1620},"          text: ",[1611,3141,3142],{"class":1627},"'hello world'",[1611,3144,1761],{"class":1620},[1611,3146,3148],{"class":1613,"line":3147},62,[1611,3149,3150],{"class":1620},"        },\n",[1611,3152,3154],{"class":1613,"line":3153},63,[1611,3155,3156],{"class":1620},"      ],\n",[1611,3158,3160],{"class":1613,"line":3159},64,[1611,3161,2215],{"class":1620},[1611,3163,3165],{"class":1613,"line":3164},65,[1611,3166,2220],{"class":1620},[1611,3168,3170],{"class":1613,"line":3169},66,[1611,3171,2310],{"class":1620},[1327,3173],{},[1216,3175,3176],{},"輸入完訊息，就會看到你的專屬代理人回覆 hello world！的訊息 ⭐⭐⭐",[1243,3178,3179],{},[1216,3180,3181],{},"這邊眼尖的你可能會發現我這邊沒使用到 LINE_CHANNEL_SECRET 也可以使用 LINE Bot Webhook 的功能，那為什麼還需要這個呢？明天的文章帶你揭曉",[1216,3183,3184],{},[1219,3185],{"alt":3186,"src":3187},"後端伺服器傳遞 Hello world 成功圖片","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757942267\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E5%BE%8C%E7%AB%AF%E4%BC%BA%E6%9C%8D%E5%99%A8%E5%82%B3%E9%81%9E_Hello_world_%E6%88%90%E5%8A%9F%E5%9C%96%E7%89%87_nh2myy.png",[3189,3190,3191],"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 .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":1433,"searchDepth":134,"depth":134,"links":3193},[3194],{"id":1226,"depth":329,"text":1226,"children":3195},[3196,3197,3212,3213,3214],{"id":1258,"depth":318,"text":1259},{"id":1288,"depth":318,"text":1289,"children":3198},[3199,3200,3201,3202,3203,3204,3205,3206,3208,3209,3210,3211],{"id":1332,"depth":134,"text":1333},{"id":1389,"depth":134,"text":1390},{"id":1414,"depth":134,"text":1415},{"id":1436,"depth":134,"text":1437},{"id":1459,"depth":134,"text":1460},{"id":1493,"depth":134,"text":1494},{"id":1508,"depth":134,"text":1509},{"id":1523,"depth":134,"text":3207},"Step 2-6：創建一個檔案命名.env並且在其中貼上以下兩行",{"id":1569,"depth":134,"text":1570},{"id":1860,"depth":134,"text":1861},{"id":2003,"depth":134,"text":2004},{"id":2313,"depth":134,"text":2314},{"id":2364,"depth":318,"text":2365},{"id":2402,"depth":318,"text":2403},{"id":2423,"depth":318,"text":2424},[11,362],"md",{},"---\ntitle: \"Day 2：從 Webhook 到後端，打造你的專屬機器人\"\ndate: 2025-09-15 21:20:49\ntags:\n  - \"iThome 鐵人賽\"\n  - \"2025 鐵人賽\"\n  - \"Line Bot\"\n  - \"NestJS\"\ncategories:\n  - \"iThome 鐵人賽\"\n  - \"Line Bot\"\ndescription: \"上一篇，帶大家透過「Chat 模式」，讓自身變成客服機器人的方式。這篇會從「Webhook 模式」切入，並以 NestJS 作為後端框架，教你如何在休息時間，也能讓後端伺服器當你的替身幫你解惑使用者的問題。\"\ncanonical: https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10380524\nseries:\n  name: \"2025 iThome 鐵人賽：Line Bot × NestJS：30 天開發日記\"\n  url: https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F8779\n  day: 2\n---\n\n![2025 鐵人賽背景圖](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1758030949\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E5%BE%9E_Webhook_%E5%88%B0%E5%BE%8C%E7%AB%AF_%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA_refdrd.png)\n\n## 前言\n\n上一篇，帶大家透過「Chat 模式」，讓自身變成客服機器人的方式。這篇會從「Webhook 模式」切入，並以 NestJS 作為後端框架，教你如何在休息時間，也能讓後端伺服器當你的替身幫你解惑使用者的問題。\n\n本文將著重於實現以下核心功能：\n\n- 使用後端伺服器接收 LINE 伺服器推送的事件\n- 根據 LINE 伺服器訊息事件自動回覆使用者「Hello World」\n\n> 本日程式碼的[範例連結](https:\u002F\u002Fgithub.com\u002FLING-JUN-HAO\u002Fnestjs-linebot-ironman)\n\n### Step 1：啟用 LINE Official Account webhook 功能\n\n> 預設 webhook 功能是關閉，需要設置完 webhook URL 才可以啟用這個功能\n\n![LINE Official Account webhook 設定](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757928323\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_Official_Account_webhook_%E8%A8%AD%E5%AE%9A_vswo6w.jpg)\n\n> 這邊會讓你選擇 Provider，這邊我選擇使用 Day1 創建的`Antonio(2025 IT 鐵人賽專用)`\n\n![LINE Official Account Messaging API Provider 選擇](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757928376\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_Official_Account_Messaging_API_Provider_%E9%81%B8%E6%93%87_hlh5cl.png)\n\n### Step 2：設定你的 webhook URL 連結\n\n> Webhook URL 用於建立後端伺服器與 LINE 平台的連結關係\n\n完成 webhook Provider 設定後，如果回到 LINE Response Setting 頁面，你會發現原本說明已經改變，這表示已經進入到 webhook 設定的最後一個步驟：設置 webhook URL。\n\n接下來的所有設定操作都會改在 LINE Developers 平台進行。雖然 webhook URL 在`LINE Developers` 和 `LINE Official Account Manager` 兩個平台都可以設定，但我們選擇在 LINE Developers 進行的主要原因是在 LINE developers 有驗證連線的按鈕可以確認連線是否成功。\n\n![LINE Official Account 啟用成功畫面](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1758022241\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_Official_Account_%E5%95%9F%E7%94%A8%E6%88%90%E5%8A%9F%E7%95%AB%E9%9D%A2%E9%A1%AF%E7%A4%BA_nf88mr.jpg)\n\n> LINE Official Account Response settings 說明改變\n\n![Line Official Account webhook 選項文字](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757929669\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLine_Official_Account_webhook_%E9%81%B8%E9%A0%85%E6%96%87%E5%AD%97_as14bh.png)\n\n* * *\n\n#### Webhook 與 LINE Bot 之間的關係\n\n首先，這邊會帶著大家理解 LINE 平台與後端伺服器的關係，後端伺服器就像是一個 24hr 不休息的員工，我們可以讓他跟 LINE 平台連接，藉此達到即使不在電腦前面，當收到 LINE 官方帳號的相關事件(新朋友加入、封鎖、傳送訊息)伺服器也能代替我們處理。\n\n不僅如此，伺服器能夠按照我們所想的邏輯或是搭配 AI 回覆，讓回覆使用者的內容不要那麼像是罐頭訊息，也可以根據不同的使用情境(例如：聖誕節)動態調整回覆訊息的風格。\n\n以下是 LINE 平台跟後端伺服器搭配運作的關係圖：\n\n> LINE 官方帳號訊息處理採用「事件驅動」的架構，透過 Webhook 機制實現即時的雙向溝通。  \n> (白話解釋： Webhook URL 就像是你的官方帳號頻道與 LINE 平台之間的專屬橋樑。當有使用者與官方帳號互動時，LINE 平台會主動透過這座橋樑將訊息推送到你的伺服器進行處理，而不需要你的伺服器不斷詢問「有沒有新訊息？」\n\n![LINE 平台與後端伺服器傳送訊息流程圖](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757930056\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLine_%E5%B9%B3%E5%8F%B0%E8%88%87%E5%BE%8C%E7%AB%AF%E4%BC%BA%E6%9C%8D%E5%99%A8%E5%82%B3%E9%80%81%E8%A8%8A%E6%81%AF%E6%B5%81%E7%A8%8B%E5%9C%96_akhtrf.png)\n\n**LINE 傳送訊息流程解釋：**\n\n> 1.  **用戶發送訊息**：使用者透過 LINE 應用程式發送一則訊息。\n> 2.  **LINE 平台事件轉發**：當使用者與 LINE 官方帳號互動時，LINE 平台會根據綁定的 Channel ID(頻道識別碼)，將事件資料以 HTTP POST 方式傳送至頻道設定的 Webhook URL。伺服器接收到這些事件後，可以進行相對應的處理與回應。\n> 3.  **伺服器回應處理**：當伺服器完成訊息處理後，透過 LINE Messaging API 將回應訊息傳送回 LINE 平台，最終傳達給使用者。\n\n#### 動手來做一個後端伺服器吧！\n\n> 前期會搭配使用 ngrok 的方式，方便大家快速看到 Demo 成效\n\n首先，透過 NestJS CLI 快速建立後端的環境，讓大家快速體會使用 Webhook 與 LINE Bot 交互的感覺。\n\n> 使用的環境配置如下：\n> \n> -   nodejs 版本：v20.18.1\n> -   nvm 版本：0.39.1\n\n#### Step 2-1：輸入指令安裝 NestCli\n\n> 選擇你喜歡的套件管理器：如果不知道用哪一個推薦使用`pnpm`\n\n```\npnpm add -g @nestjs\u002Fcli\n```\n\n#### Step 2-2：輸入指令安裝 NestCli\n\n```\nnest new linebot-webhook-server \u002F\u002F 創建一個 nest專案，名稱為 linebot-webhook-server\n```\n\n看到終端機寫著`Successfully created`代表已經創立好專案囉！  \n![nest cli 完成畫面](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757941182\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002Fnest_cli_%E5%AE%8C%E6%88%90%E7%95%AB%E9%9D%A2_eqfsqp.png)\n\n#### Step 2-3：按照 nest cli 的指示執行以下兩個指令\n\n```\ncd linebot-webhook-server \u002F\u002F 進入這個創建完的資料夾中\npnpm run start:dev \u002F\u002F 啟動這個 nest 專案(支援熱重載功能)\n```\n\n看到終端機寫著`Nest application successfully started`代表伺服器成功啟動！  \n![開啟 NestJS 專案伺服器](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757941216\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E9%96%8B%E5%95%9F_NestJS_%E5%B0%88%E6%A1%88%E4%BC%BA%E6%9C%8D%E5%99%A8_vbmbya.png)\n\n> 預設伺服器啟動的網址是：http:\u002F\u002Flocalhost:3000  \n> (看到 hello world 就代表順利透過後端伺服器取回預設回傳值)\n\n#### Step 2-4：安裝 LINE sdk(LINE Bot 相關套件)\n\n> 記得先關掉伺服器(control + c)，再繼續安裝\n\n```\npnpm install @line\u002Fbot-sdk --save\n```\n\n#### Step 2-5：安裝處理環境變數的套件\n\n> 白話解釋：私人秘密不想被別人看到的檔案\n\n```\npnpm install @nestjs\u002Fconfig\n```\n\n#### Step 2-6：創建一個檔案命名`.env`並且在其中貼上以下兩行\n\n> -   LINE\\_CHANNEL\\_SECRET：驗證請求後端伺服器來源的是 LINE 平台\n> -   LINE\\_CHANNEL\\_ACCESS\\_TOKEN：驗證頻道授權者，可以透過 Bot 與使用者互動\n\n```\nLINE_CHANNEL_ACCESS_TOKEN=你的 LINE Bot 頻道存取權杖\nLINE_CHANNEL_SECRET=你的 LINE Bot 頻道密鑰\n```\n\n**1️⃣ LINE\\_CHANNEL\\_SECRET 放在 Basic settings 頁籤：**  \n![LINE\\_CHANNEL\\_SECRET](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757941436\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_CHANNEL_SECRET_ddedbd.png)\n\n**2️⃣ LINE\\_CHANNEL\\_ACCESS\\_TOKEN 放在 Messaging API 頁籤：**  \n![LINE\\_CHANNEL\\_ACCESS\\_TOKEN](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757941478\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FLINE_CHANNEL_ACCESS_TOKEN_nialr0.png)\n\n#### Step 2-7：極簡化伺服器資料夾結構 + 註冊環境變數\n\n> 將檔案結構調整成最簡單能運行的方式，在逐漸優化！\n\n- 刪除`app.controller.spec`及`app.service`的部分\n- 創建 config 資料夾及 line.config.ts 檔案\n\n![初始化檔案結構](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757930313\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002FNestJS_%E5%88%9D%E5%A7%8B%E5%8C%96%E6%AA%94%E6%A1%88%E7%B5%90%E6%A7%8B_uhjjsk.png)\n\n**line.config.ts 內容**\n\n```ts\nimport { ClientConfig } from '@line\u002Fbot-sdk';\nimport { ConfigService } from '@nestjs\u002Fconfig';\n\n\u002F\u002F 註冊名稱\nexport const LINE_CONFIG = 'LINE_CONFIG';\n\n\u002F\u002F 註冊的常數(這邊讀取的是.env 環境變數裡面的內容)\nconst lineConfig = (configService: ConfigService): ClientConfig => ({\n  channelAccessToken:\n    configService.get\u003Cstring>('LINE_CHANNEL_ACCESS_TOKEN') ?? '',\n  channelSecret: configService.get\u003Cstring>('LINE_CHANNEL_SECRET') ?? '',\n});\n\n\u002F\u002F 匯出成 NestJS Provider 供依賴注入系統使用(讓 NestJS 可以透過 DI 容器管理此設定)\nexport const LineConfigProvider = {\n  provide: LINE_CONFIG,\n  useFactory: (configService: ConfigService) => lineConfig(configService),\n  inject: [ConfigService],\n};\n```\n\n#### Step 2-8：將 LINE Config Provider 註冊到根模組中使用\n\n- **ConfigModule.forRoot({ isGlobal: true })**：全域註冊環境變數，讓整個應用程式都能存取環境變數\n- **LINE Config Provider 註冊**：將 LineConfigProvider 加入到 providers 陣列中，讓它可以被依賴注入系統使用。\n\n**src\u002Fapp.module.ts：**\n\n```ts\nimport { Module } from '@nestjs\u002Fcommon';\nimport { AppController } from '.\u002Fapp.controller';\nimport { ConfigModule } from '@nestjs\u002Fconfig';\nimport { LineConfigProvider } from '..\u002Fconfig\u002Fline.config';\n\n@Module({\n  imports: [ConfigModule.forRoot({ isGlobal: true })],\n  controllers: [AppController],\n  providers: [LineConfigProvider],\n})\nexport class AppModule {}\n```\n\n#### Step 2-9：設定 \u002Fwebhook 路徑接收 LINE 平台傳遞的事件\n\n這裡設定的路由前綴為 `\u002Fwebhook`，這是專門用來接收 LINE Bot webhook 事件的端點路徑。當 LINE 平台有訊息或事件需要推送給我們的應用程式時，會透過這個特定的路由路徑進行回調通知\n\n**src\u002Fapp.controller.ts：**\n\n```ts\nimport { Body, Controller, Inject, Post } from '@nestjs\u002Fcommon';\nimport {\n  ClientConfig,\n  WebhookRequestBody,\n  messagingApi,\n  MessageEvent,\n  WebhookEvent,\n} from '@line\u002Fbot-sdk';\nimport { LINE_CONFIG } from '..\u002Fconfig\u002Fline.config';\n\n@Controller()\nexport class AppController {\n  private readonly lineClient: messagingApi.MessagingApiClient;\n\n  \u002F\u002F 根據配置檔案初始化 LINE Messaging API 客戶端\n  constructor(@Inject(LINE_CONFIG) private readonly lineConfig: ClientConfig) {\n    this.lineClient = new messagingApi.MessagingApiClient({\n      channelAccessToken: this.lineConfig.channelAccessToken,\n    });\n  }\n\n  @Post('\u002Fwebhook')\n  handleWebhook(@Body() body: WebhookRequestBody): Promise\u003Cstring> {\n    console.log('Webhook 從 line platform 接收到的資訊:', body);\n    return 'Webhook processed successfully';\n  }\n}\n```\n\n#### Step 2-10：Nest 後端伺服器開啟的狀態下搭配使用 ngrok\n\n> LINE Webhook URL 必須是 HTTPS 網址，所以這邊需要搭配 ngrok 使用\n\n`ngrok`是一個反向代理工具，能夠將本地端口轉發至公開的`HTTPS 網址`。它提供第三方代理服務，讓外部用戶可以直接透過公開網址存取您本地運行的應用程式\n\n使用指令非常簡單，僅需要打開終端機輸入以下指令：\n\n```\nngrok http 3000\n```\n\n看到以下顯示的樣子就代表已經成功建立公開網址  \n![ngrok 運行畫面](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757940102\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002Fngrok_%E9%81%8B%E8%A1%8C%E7%95%AB%E9%9D%A2_cmndnz.png)\n\n**這個`https:\u002F\u002Fd120-49-213-168-27.ngrok-free.app`就是 webhook URL**\n\n> 這種方式屬於臨時伺服器的方式，連結有時候會斷開。如果重新開啟 ngrok 需要記得 https 連結位置會改變，不要忘記連帶改動設定的 Webhook URL\n\n### Step 3：在 LINE developers 設置 webhook URL 並測試\n\n登入 [LINE developers](https:\u002F\u002Fdevelopers.line.biz\u002Fzh-hant\u002F) 後，在 `Message API Webhook settings`可以設定 Webhook URL：  \n![在 LINE developers 設置 Webhook URL](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757940044\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E5%9C%A8_Line_developers_%E8%A8%AD%E7%BD%AE_Webhook_URL_gvhmnd.png)\n\n> 特別注意要加上 `\u002Fwebhook` 的路徑前綴，這是我們後端設置接收 LINE 事件的端點，這樣才可以正確打到架設的後端伺服器喔！\n\n按下`Verify 按鈕`的同時，LINE 平台就會發送事件到你的後端伺服器，這時候只要後端伺服器回傳的請求狀態碼是 200，畫面上會顯示 Success。這時候就代表已經成功建立 LINE 平台與你架設的後端伺服器之間的連結。\n\n### Step 4：在 LINE developers 打開 Use webhook 按鈕\n\n在剛才設置 Webhook URL 的下方，找到並開啟「Use webhook」開關。此設定會與先前在 Line Official Account Manager 中的配置進行連動。\n\n* * *\n\n開啟後，你可以試著在官方帳號輸入訊息，查看終端機印出的訊息：\n\n![後端伺服器終端機印出 LINE 平台送出的事件](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757940193\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E5%BE%8C%E7%AB%AF%E4%BC%BA%E6%9C%8D%E5%99%A8%E7%B5%82%E7%AB%AF%E6%A9%9F%E5%8D%B0%E5%87%BA_Line_%E5%B9%B3%E5%8F%B0%E9%80%81%E5%87%BA%E7%9A%84%E4%BA%8B%E4%BB%B6_cqcwpa.png)\n\n印出收到的訊息，代表你已經能成功接收到使用者跟你的 LINE 官方帳號互動的事件囉！\n\n### Step 5：修改 app.controller.ts 回覆使用者 Hello World！\n\nLINE Bot 與使用者的互動包含多種事件類型，這邊舉例三種最常見的事件：\n\n- **Follow event**：用戶加入 LINE 官方帳號\n- **Unfollow event**：用戶封鎖 LINE 官方帳號\n- **Message event**：用戶傳送訊息\n\n本系列的文章將聚焦於最常見的使用情境 `Message event`。\n\n在下面程式碼中，我們會特別展示一個重要概念：每則訊息都帶有專屬的 **Reply Token**（可以理解為該訊息的「身分證」）。後端伺服器可以利用這個 Token 來回應特定的用戶訊息，這就是 LINE Bot 回覆訊息的第一種方式 **Reply Message**。\n\n**src\u002Fapp.controller.ts：**\n\n```ts\n\u002F\u002F...略\n\n\u002F\u002F 限制型別先收斂成部分事件，搭配 eventHandler 使用\ntype HandlerMap = {\n  [K in WebhookEvent['type']]: (\n    event: Extract\u003CWebhookEvent, { type: K }>,\n  ) => Promise\u003Cvoid>;\n};\n\n@Controller()\nexport class AppController {\n  private readonly lineClient: messagingApi.MessagingApiClient;\n\n  \u002F\u002F 根據配置檔案初始化 LINE Messaging API 客戶端\n  constructor(@Inject(LINE_CONFIG) private readonly lineConfig: ClientConfig) {\n    this.lineClient = new messagingApi.MessagingApiClient({\n      channelAccessToken: this.lineConfig.channelAccessToken,\n    });\n  }\n\n  \u002F**\n   * Webhook 端點處理器\n   * 接收來自 LINE Platform 的事件通知\n   * @param body LINE Platform 傳送的 Webhook 請求本體\n   * @returns 處理完成的回應訊息\n   *\u002F\n  @Post('\u002Fwebhook')\n  async handleWebhook(@Body() body: WebhookRequestBody): Promise\u003Cstring> {\n    console.log('Webhook 從 LINE 平台接收到的資訊:', body);\n    const { events } = body;\n\n    \u002F\u002F 事件處理器映射表\n    const eventHandler = {\n      message: (event) => this.handleMessageEvent(event),\n    } satisfies Partial\u003CHandlerMap>;\n\n    \u002F\u002F 逐項處理每一個事件\n    for (const event of events) {\n      const { type } = event;\n      if (type === 'message') {\n        await eventHandler[type](event);\n      }\n    }\n\n    return 'Webhook processed successfully';\n  }\n\n  \u002F**\n   * 處理訊息事件\n   * @param event 訊息事件物件，包含用戶訊息內容(text)和回覆憑證(replyToken)\n   *\u002F\n  private async handleMessageEvent(event: MessageEvent): Promise\u003Cvoid> {\n    const { replyToken } = event;\n    console.log('收到訊息事件', event);\n    console.log('訊息憑證(身分證):', replyToken);\n    await this.lineClient.replyMessage({\n      replyToken,\n      messages: [\n        {\n          type: 'text',\n          text: 'hello world',\n        },\n      ],\n    });\n  }\n}\n```\n\n* * *\n\n輸入完訊息，就會看到你的專屬代理人回覆 hello world！的訊息 ⭐⭐⭐\n\n> 這邊眼尖的你可能會發現我這邊沒使用到 LINE\\_CHANNEL\\_SECRET 也可以使用 LINE Bot Webhook 的功能，那為什麼還需要這個呢？明天的文章帶你揭曉\n\n![後端伺服器傳遞 Hello world 成功圖片](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1757942267\u002F2025%20IT%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%202%EF%BC%9A%E5%BE%9E%20webhook%20%E5%88%B0%E5%BE%8C%E7%AB%AF%EF%BC%8C%E6%89%93%E9%80%A0%E4%BD%A0%E7%9A%84%E5%B0%88%E5%B1%AC%E6%A9%9F%E5%99%A8%E4%BA%BA\u002F%E5%BE%8C%E7%AB%AF%E4%BC%BA%E6%9C%8D%E5%99%A8%E5%82%B3%E9%81%9E_Hello_world_%E6%88%90%E5%8A%9F%E5%9C%96%E7%89%87_nh2myy.png)\n",{"title":643,"description":644},{"name":366,"url":367,"day":329},{"loc":642,"images":3222,"lastmod":645},[3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237],{"loc":1222},{"loc":1270},{"loc":1285},{"loc":1314},{"loc":1325},{"loc":1357},{"loc":1456},{"loc":1478},{"loc":1555},{"loc":1566},{"loc":1597},{"loc":2347},{"loc":2384},{"loc":2417},{"loc":3187},"ithome-2025-day-02","2025-09-15-ithome-2025-day-02",[11,360,362,350],"jXdeG5PMm6eziRTw4P7NoJb1dQhII4-FJ5RjwePDe7g",1792575196903]