[{"data":1,"prerenderedAt":1508},["ShallowReactive",2],{"navigation_docs":3,"-web-dev-network-sse-introduction":352,"-web-dev-network-sse-introduction-surround":1503},[4,54,83,116,125,161,179,200],{"title":5,"path":6,"stem":7,"children":8},"Ai","/ai","ai",[9,12,42,46,50],{"title":10,"path":6,"stem":11},"AI 技術探索","ai/index",{"title":13,"path":14,"stem":15,"children":16,"page":41},"Rag","/ai/rag","ai/RAG",[17,21,25,29,33,37],{"title":18,"path":19,"stem":20},"BM25 演算法深入解析：從 TF-IDF 到現代搜尋引擎的核心技術","/ai/rag/bm25-deep-dive-from-tf-idf-to-rag","ai/RAG/bm25-deep-dive-from-tf-idf-to-rag",{"title":22,"path":23,"stem":24},"Dense Retrieval（向量檢索）深入解析","/ai/rag/dense-retrieval-deep-dive","ai/RAG/dense-retrieval-deep-dive",{"title":26,"path":27,"stem":28},"RAG（Retrieval-Augmented Generation）原理與實作入門","/ai/rag/rag-intro-principles-and-implementation","ai/RAG/rag-intro-principles-and-implementation",{"title":30,"path":31,"stem":32},"RAG Retrieval Strategy 深入解析","/ai/rag/rag-retrieval-strategy-deep-dive","ai/RAG/rag-retrieval-strategy-deep-dive",{"title":34,"path":35,"stem":36},"Reciprocal Rank Fusion（RRF）教學：打造更穩定的檢索融合策略","/ai/rag/rrf-reciprocal-rank-fusion-guide","ai/RAG/rrf-reciprocal-rank-fusion-guide",{"title":38,"path":39,"stem":40},"TF-IDF（Term Frequency–Inverse Document Frequency）深入解析","/ai/rag/tf-idf-deep-dive","ai/RAG/tf-idf-deep-dive",false,{"title":43,"path":44,"stem":45},"Docus AI 功能完整實作指南","/ai/docus-ai-implementation","ai/docus-ai-implementation",{"title":47,"path":48,"stem":49},"llms.txt 是什麼？在 Nuxt 中控制 AI 與搜尋引擎爬蟲的新方式","/ai/llms-txt","ai/llms-txt",{"title":51,"path":52,"stem":53},"MCP（Model Context Protocol）是什麼？用「AI 的 USB-C」理解模型如何安全連接工具與資料","/ai/mcp_concept","ai/mcp_concept",{"title":55,"path":56,"stem":57,"children":58,"page":41},"Data Analysis","/data-analysis","data-analysis",[59,63,67,71,75,79],{"title":60,"path":61,"stem":62},"Exploratory Data Analysis（EDA）資料探索分析入門","/data-analysis/exploratory-data-analysis-eda","data-analysis/exploratory-data-analysis-eda",{"title":64,"path":65,"stem":66},"PR-AUC 深入解析：不平衡分類問題的重要評估指標","/data-analysis/pr-auc-for-imbalanced-classification","data-analysis/pr-auc-for-imbalanced-classification",{"title":68,"path":69,"stem":70},"ROC-AUC 深入解析：理解分類模型的判斷能力","/data-analysis/roc-auc-complete-guide","data-analysis/roc-auc-complete-guide",{"title":72,"path":73,"stem":74},"SHAP（SHapley Additive exPlanations）模型解釋方法深入解析","/data-analysis/shap-model-interpretation-complete-guide","data-analysis/shap-model-interpretation-complete-guide",{"title":76,"path":77,"stem":78},"Social Network Analysis（SNA）深入解析","/data-analysis/social-network-analysis-sna","data-analysis/social-network-analysis-sna",{"title":80,"path":81,"stem":82},"Time-based Split：為什麼時間序列資料不能使用 Random Split？","/data-analysis/time-based-split-vs-random-split","data-analysis/time-based-split-vs-random-split",{"title":84,"path":85,"stem":86,"children":87,"page":41},"MachineLearning","/machine_learning","machine_learning",[88,92,96,100,104,108,112],{"title":89,"path":90,"stem":91},"Confusion Matrix（混淆矩陣）完整解析","/machine_learning/confusion-matrix","machine_learning/confusion-matrix",{"title":93,"path":94,"stem":95},"Decision Tree 模型完整教學：從原理到 Python 實作","/machine_learning/decision-tree-complete-guide","machine_learning/decision-tree-complete-guide",{"title":97,"path":98,"stem":99},"為什麼 Decision Tree 在 Tabular Data 上常常勝過 Deep Learning？","/machine_learning/decision-tree-vs-deep-learning-on-tabular-data","machine_learning/decision-tree-vs-deep-learning-on-tabular-data",{"title":101,"path":102,"stem":103},"LightGBM 模型入門：理解高效能的 Gradient Boosting 演算法","/machine_learning/lightgbm-intro-for-tabular-data","machine_learning/lightgbm-intro-for-tabular-data",{"title":105,"path":106,"stem":107},"Logistic Regression（Logit 回歸）完整教學","/machine_learning/logistic-regression-complete-guide","machine_learning/logistic-regression-complete-guide",{"title":109,"path":110,"stem":111},"為什麼模型訓練需要 Train / Validation / Test Dataset？","/machine_learning/train-validation-test-dataset","machine_learning/train-validation-test-dataset",{"title":113,"path":114,"stem":115},"XGBoost 原理與實務教學","/machine_learning/xgboost-complete-guide","machine_learning/xgboost-complete-guide",{"title":117,"path":118,"stem":119,"children":120,"page":41},"Network","/network","network",[121],{"title":122,"path":123,"stem":124},"DNS 設定入門：CNAME vs A Record 完整解析","/network/dns-cname-vs-a-record-guide","network/dns-cname-vs-a-record-guide",{"title":126,"path":127,"stem":128,"children":129},"Nuxt","/nuxt","nuxt",[130,133,137,141,145,149,153,157],{"title":131,"path":127,"stem":132},"Nuxt 實戰指南","nuxt/index",{"title":134,"path":135,"stem":136},"Abstraction Layer 與 Adapter 的差別，一次搞懂設計角色","/nuxt/adapter_abstraction_layer","nuxt/adapter_abstraction_layer",{"title":138,"path":139,"stem":140},"Nuxt 4 全面理解 — 專案檔案結構解析","/nuxt/directory_structure","nuxt/directory_structure",{"title":142,"path":143,"stem":144},"Nuxt 前端 build 流程完整解析","/nuxt/frontend_build","nuxt/frontend_build",{"title":146,"path":147,"stem":148},"Nitro 是什麼？從 Nuxt 專案到 Server 與 Edge 的關鍵引擎","/nuxt/nitro","nuxt/nitro",{"title":150,"path":151,"stem":152},"深入理解 Nuxt 的 .nuxt 與 .output：為什麼部署時一定要分清楚？","/nuxt/nuxt_and_output","nuxt/nuxt_and_output",{"title":154,"path":155,"stem":156},"Nuxt 中 dev、build、preview 的差異一次搞懂","/nuxt/nuxt_dev_build_preview","nuxt/nuxt_dev_build_preview",{"title":158,"path":159,"stem":160},"Nuxt Image vs \u003Cimg>：為什麼 Nuxt 3 專案幾乎都該用 \u003CNuxtImg>？","/nuxt/nuxt_image","nuxt/nuxt_image",{"title":162,"path":163,"stem":164,"children":165},"Python 程式開發","/python","python/index",[166,167,171,175],{"title":162,"path":163,"stem":164},{"title":168,"path":169,"stem":170},"如何在 FastAPI 中調用記憶體 Buffer 回傳圖片 (附實例)","/python/memory_buffer","python/memory_buffer",{"title":172,"path":173,"stem":174},"Python 單元測試教學：如何為你的程式撰寫 Test","/python/python-unit-testing-unittest-pytest","python/python-unit-testing-unittest-pytest",{"title":176,"path":177,"stem":178},"如何使用 uv 取代 pip：改善 Python 專案的開發流程","/python/uv","python/uv",{"title":180,"path":181,"stem":182,"children":183,"page":41},"Statistic","/statistic","statistic",[184,188,192,196],{"title":185,"path":186,"stem":187},"卡方檢定（Chi-Square Test）入門教學","/statistic/chi-square-test-intro","statistic/chi-square-test-intro",{"title":189,"path":190,"stem":191},"常見抽樣方法（Sampling Methods）教學：Stratified、Quota、Convenience、Systematic、Simple Random","/statistic/common-sampling-methods-guide","statistic/common-sampling-methods-guide",{"title":193,"path":194,"stem":195},"Cramér’s V 指標介紹：如何衡量兩個類別變數之間的關聯","/statistic/cramers-v-for-categorical-association","statistic/cramers-v-for-categorical-association",{"title":197,"path":198,"stem":199},"Mann–Whitney U 與 Kolmogorov–Smirnov（KS）檢定：非參數統計檢定的入門指南","/statistic/mann-whitney-u-and-ks-test-intro","statistic/mann-whitney-u-and-ks-test-intro",{"title":201,"path":202,"stem":203,"children":204},"WebDev","/web_dev","web_dev",[205,208,234,248,282,312,334],{"title":206,"path":202,"stem":207},"Web 前端開發","web_dev/index",{"title":209,"path":210,"stem":211,"children":212},"瀏覽器與渲染","/web_dev/browser","web_dev/browser/index",[213,214,218,222,226,230],{"title":209,"path":210,"stem":211},{"title":215,"path":216,"stem":217},"瀏覽器儲存空間完整解析：Cookie、localStorage、IndexedDB 到 Cache Storage","/web_dev/browser/browser-storage-comprehensive-guide","web_dev/browser/browser-storage-comprehensive-guide",{"title":219,"path":220,"stem":221},"DOM (Document Object Model) 深入解析","/web_dev/browser/dom","web_dev/browser/dom",{"title":223,"path":224,"stem":225},"Service Worker Request Flow 深入解析","/web_dev/browser/service-worker-request-flow","web_dev/browser/service-worker-request-flow",{"title":227,"path":228,"stem":229},"CSR、SSR 與 SSG 是什麼？前端渲染策略完整比較","/web_dev/browser/ssr_csr_ssg","web_dev/browser/ssr_csr_ssg",{"title":231,"path":232,"stem":233},"Virtual DOM 深入解析：為什麼它能優化前端效能？","/web_dev/browser/virtual_dom","web_dev/browser/virtual_dom",{"title":235,"path":236,"stem":237,"children":238},"圖形技術","/web_dev/graphics","web_dev/graphics/index",[239,240,244],{"title":235,"path":236,"stem":237},{"title":241,"path":242,"stem":243},"為什麼 Three.js 專案幾乎都選擇 CSR？從 SSR 問題談起","/web_dev/graphics/threejs_csr","web_dev/graphics/threejs_csr",{"title":245,"path":246,"stem":247},"WebGL 是什麼？為什麼前端 3D 幾乎都靠它？","/web_dev/graphics/webgl","web_dev/graphics/webGL",{"title":249,"path":250,"stem":251,"children":252},"架構與配置","/web_dev/infrastructure","web_dev/infrastructure/index",[253,254,258,262,266,270,274,278],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257},"Zeabur + K3s + Nuxt 部署架構完整解析","/web_dev/infrastructure/k3s-zeabur","web_dev/infrastructure/K3s-zeabur",{"title":259,"path":260,"stem":261},"ECS vs Docker vs Kubernetes：從部署堆疊理解三層架構","/web_dev/infrastructure/ecs-docker-kubernetes-stack","web_dev/infrastructure/ecs-docker-kubernetes-stack",{"title":263,"path":264,"stem":265},"ECS vs VPS 是什麼？從部署網站的角度一次搞懂差別","/web_dev/infrastructure/ecs-vs-vps","web_dev/infrastructure/ecs-vs-vps",{"title":267,"path":268,"stem":269},"Zeabur 內網服務連線完整入門指南","/web_dev/infrastructure/k3s-internal-networking","web_dev/infrastructure/k3s-internal-networking",{"title":271,"path":272,"stem":273},"使用 Microservices 架構設計系統的優勢解析","/web_dev/infrastructure/microservices-architecture-advantages","web_dev/infrastructure/microservices-architecture-advantages",{"title":275,"path":276,"stem":277},"Nginx 入門教學：從反向代理到與 Kubernetes 的架構比較","/web_dev/infrastructure/nginx-intro","web_dev/infrastructure/nginx-intro",{"title":279,"path":280,"stem":281},"YAML 配置是什麼？為什麼現代開發都在用它","/web_dev/infrastructure/yaml-configuration","web_dev/infrastructure/yaml-configuration",{"title":283,"path":284,"stem":285,"children":286},"網絡與通訊","/web_dev/network","web_dev/network/index",[287,288,292,296,300,304,308],{"title":283,"path":284,"stem":285},{"title":289,"path":290,"stem":291},"HTTP Request 結構完整解析：從 Request Line 到 Header 一次看懂","/web_dev/network/http-request-structure","web_dev/network/http-request-structure",{"title":293,"path":294,"stem":295},"OSI 模型（Open Systems Interconnection Model）完整解析","/web_dev/network/osi-model","web_dev/network/osi-model",{"title":297,"path":298,"stem":299},"RESTful API 設計原則與實務解析","/web_dev/network/restful-api","web_dev/network/restful-api",{"title":301,"path":302,"stem":303},"RESTful API 五大設計原則深入解析","/web_dev/network/restful-api-principles","web_dev/network/restful-api-principles",{"title":305,"path":306,"stem":307},"Server-Sent Events (SSE) 深入解析與實作教學","/web_dev/network/sse-introduction","web_dev/network/sse-introduction",{"title":309,"path":310,"stem":311},"WebSocket 入門教學：從概念到 Node.js 實作","/web_dev/network/websocket-introduction","web_dev/network/websocket-introduction",{"title":313,"path":314,"stem":315,"children":316},"認證與安全","/web_dev/security","web_dev/security/index",[317,318,322,326,330],{"title":313,"path":314,"stem":315},{"title":319,"path":320,"stem":321},"使用 Cookie 與 Session 建立使用者驗證（完整新手教學）","/web_dev/security/cookie-session-authentication","web_dev/security/cookie-session-authentication",{"title":323,"path":324,"stem":325},"JWT 驗證機制完整解析：從登入流程到實務應用","/web_dev/security/jwt-authentication","web_dev/security/jwt-authentication",{"title":327,"path":328,"stem":329},"Secret Key 簽名是什麼？從零理解資料簽名的本質","/web_dev/security/secret-key-signing","web_dev/security/secret-key-signing",{"title":331,"path":332,"stem":333},"SSH（Secure Shell）是什麼？從遠端登入到安全通道的核心概念","/web_dev/security/ssh","web_dev/security/ssh",{"title":335,"path":336,"stem":337,"children":338},"SEO 與規範","/web_dev/seo","web_dev/seo/index",[339,340,344,348],{"title":335,"path":336,"stem":337},{"title":341,"path":342,"stem":343},"Canonical URL 是什麼？用生活化方式搞懂前端 SEO 的基本保命符","/web_dev/seo/canonical_link","web_dev/seo/canonical_link",{"title":345,"path":346,"stem":347},"robots.txt 是什麼？SEO 的第一道守門員","/web_dev/seo/robot_txt","web_dev/seo/robot_txt",{"title":349,"path":350,"stem":351},"Nuxt SEO 中的 Meta 與 SEO 工具是如何運作的？","/web_dev/seo/seo","web_dev/seo/seo",{"id":353,"title":305,"body":354,"description":1490,"extension":1491,"links":1492,"meta":1493,"navigation":597,"path":306,"seo":1501,"stem":307,"__hash__":1502},"docs/web_dev/network/sse-introduction.md",{"type":355,"value":356,"toc":1476},"minimark",[357,361,366,374,381,384,431,434,437,440,444,454,457,465,468,471,492,495,512,514,518,521,524,565,572,574,578,581,604,611,629,631,635,638,645,958,961,976,983,985,989,996,999,1168,1171,1180,1182,1186,1189,1192,1234,1237,1297,1300,1302,1306,1309,1395,1398,1400,1404,1407,1428,1430,1434,1437,1440,1445,1448,1450,1453,1456,1458,1461,1472],[358,359,305],"h1",{"id":360},"server-sent-events-sse-深入解析與實作教學",[362,363,365],"h2",{"id":364},"為什麼我們需要-sse","為什麼我們需要 SSE？",[367,368,369,370],"p",{},"在設計即時系統時，我們通常會遇到一個問題：",[371,372,373],"strong",{},"資料更新要如何從伺服器傳到瀏覽器？",[367,375,376,377,380],{},"最傳統的方式是一般 HTTP request-response 模型。Client 發出請求，Server 回傳資料，連線隨即關閉。這種模式非常適合取得靜態資料，例如讀取文章、登入、送出表單等等。但它有一個根本限制：",[371,378,379],{},"Server 不能主動推送資料","，只能等 Client 發問。",[367,382,383],{},"為了解決這個問題，有人會使用 polling（輪詢）。例如每秒向伺服器請求一次狀態更新：",[385,386,391],"pre",{"className":387,"code":388,"language":389,"meta":390,"style":390},"language-js shiki shiki-themes github-dark","setInterval(() => fetch(\"/status\"), 1000)\n","js","",[392,393,394],"code",{"__ignoreMap":390},[395,396,399,403,407,411,414,417,421,424,428],"span",{"class":397,"line":398},"line",1,[395,400,402],{"class":401},"svObZ","setInterval",[395,404,406],{"class":405},"s95oV","(() ",[395,408,410],{"class":409},"snl16","=>",[395,412,413],{"class":401}," fetch",[395,415,416],{"class":405},"(",[395,418,420],{"class":419},"sU2Wk","\"/status\"",[395,422,423],{"class":405},"), ",[395,425,427],{"class":426},"sDLfK","1000",[395,429,430],{"class":405},")\n",[367,432,433],{},"這種方式雖然可以達到「看起來即時」的效果，但本質上仍然是重複發送請求。它會浪費頻寬、增加伺服器負擔，而且更新頻率越高，資源消耗越大。",[367,435,436],{},"當應用場景需要「伺服器主動推送資料」時，就會出現兩種主流選擇：WebSocket 與 SSE。而當資料流是單向（Server → Client）時，SSE 往往是一個更簡潔的方案。",[438,439],"hr",{},[362,441,443],{"id":442},"什麼是-sse","什麼是 SSE？",[367,445,446,449,450,453],{},[371,447,448],{},"SSE"," 是 ",[371,451,452],{},"Server-Sent Events"," 的縮寫。顧名思義，它是一種讓伺服器持續向瀏覽器傳送事件資料的機制。",[367,455,456],{},"一句話理解：",[458,459,460],"blockquote",{},[367,461,462],{},[371,463,464],{},"SSE = 伺服器透過一條不關閉的 HTTP 連線，持續把資料推送給瀏覽器（單向）。",[367,466,467],{},"這裡的關鍵在於「單向」。SSE 只支援 Server → Client，不支援 Client → Server 的即時通訊。如果需要雙向互動，才會考慮使用 WebSocket。",[367,469,470],{},"在心智模型上可以這樣理解：",[472,473,474,481,487],"ul",{},[475,476,477,480],"li",{},[371,478,479],{},"一般 HTTP","：問一句，回一句。",[475,482,483,486],{},[371,484,485],{},"WebSocket","：雙方隨時對話。",[475,488,489,491],{},[371,490,448],{},"：Server 一直廣播，Client 一直聽。",[367,493,494],{},"這種模式非常適合：",[472,496,497,500,503,506,509],{},[475,498,499],{},"即時 log 顯示",[475,501,502],{},"AI 回覆串流（token streaming）",[475,504,505],{},"任務進度更新",[475,507,508],{},"CI/CD build log",[475,510,511],{},"Agent 執行狀態推送",[438,513],{},[362,515,517],{"id":516},"sse-的底層其實仍然是-http","SSE 的底層其實仍然是 HTTP",[367,519,520],{},"SSE 並沒有發明新的傳輸協定，它仍然建立在 HTTP 之上。這一點非常重要。",[367,522,523],{},"實作上，SSE 只是建立一個「長時間不關閉的 HTTP response」，並設定特殊的 header：",[385,525,529],{"className":526,"code":527,"language":528,"meta":390,"style":390},"language-yaml shiki shiki-themes github-dark","Content-Type: text/event-stream\nCache-Control: no-cache\nConnection: keep-alive\n","yaml",[392,530,531,543,554],{"__ignoreMap":390},[395,532,533,537,540],{"class":397,"line":398},[395,534,536],{"class":535},"s4JwU","Content-Type",[395,538,539],{"class":405},": ",[395,541,542],{"class":419},"text/event-stream\n",[395,544,546,549,551],{"class":397,"line":545},2,[395,547,548],{"class":535},"Cache-Control",[395,550,539],{"class":405},[395,552,553],{"class":419},"no-cache\n",[395,555,557,560,562],{"class":397,"line":556},3,[395,558,559],{"class":535},"Connection",[395,561,539],{"class":405},[395,563,564],{"class":419},"keep-alive\n",[367,566,567,568,571],{},"只要連線不關閉，Server 就可以持續透過這條通道傳送資料。這種方式相較於 WebSocket（使用 ",[392,569,570],{},"ws"," 協定）來說，更容易穿透防火牆與代理伺服器，也更容易部署。",[438,573],{},[362,575,577],{"id":576},"sse-的資料格式","SSE 的資料格式",[367,579,580],{},"SSE 傳送的是純文字格式，而且非常簡單。基本格式如下：",[385,582,586],{"className":583,"code":584,"language":585,"meta":390,"style":390},"language-kotlin shiki shiki-themes github-dark","data: hello\n\ndata: world\n","kotlin",[392,587,588,593,599],{"__ignoreMap":390},[395,589,590],{"class":397,"line":398},[395,591,592],{},"data: hello\n",[395,594,595],{"class":397,"line":545},[395,596,598],{"emptyLinePlaceholder":597},true,"\n",[395,600,601],{"class":397,"line":556},[395,602,603],{},"data: world\n",[367,605,606,607,610],{},"每一筆事件都必須以",[371,608,609],{},"空行","結尾。這個空行是事件的分隔符號，沒有它瀏覽器就不會觸發事件處理函式。",[367,612,613,614,617,618,621,622,625,626,628],{},"除了 ",[392,615,616],{},"data:","，SSE 還支援 ",[392,619,620],{},"event:","、",[392,623,624],{},"id:"," 等欄位，但在大多數實務應用中，",[392,627,616],{}," 就足夠使用。",[438,630],{},[362,632,634],{"id":633},"使用-nodejs-建立最小-sse-範例","使用 Node.js 建立最小 SSE 範例",[367,636,637],{},"接下來我們用 Node.js 建立一個最小可運作的 SSE 範例。這個範例會每秒向瀏覽器推送一個遞增數字。",[367,639,640,641,644],{},"建立一個 ",[392,642,643],{},"server.js","：",[385,646,648],{"className":387,"code":647,"language":389,"meta":390,"style":390},"const http = require(\"http\")\n\nhttp\n  .createServer((req, res) => {\n    if (req.url === \"/events\") {\n      res.writeHead(200, {\n        \"Content-Type\": \"text/event-stream\",\n        \"Cache-Control\": \"no-cache\",\n        Connection: \"keep-alive\",\n      })\n\n      let count = 0\n      const timer = setInterval(() => {\n        res.write(`data: ${count++}\\n\\n`)\n      }, 1000)\n\n      req.on(\"close\", () => {\n        clearInterval(timer)\n      })\n    }\n  })\n  .listen(3000)\n\nconsole.log(\"Server running at http://localhost:3000\")\n",[392,649,650,671,675,680,710,728,745,759,772,783,789,794,809,829,860,870,875,896,905,910,916,922,937,942],{"__ignoreMap":390},[395,651,652,655,658,661,664,666,669],{"class":397,"line":398},[395,653,654],{"class":409},"const",[395,656,657],{"class":426}," http",[395,659,660],{"class":409}," =",[395,662,663],{"class":401}," require",[395,665,416],{"class":405},[395,667,668],{"class":419},"\"http\"",[395,670,430],{"class":405},[395,672,673],{"class":397,"line":545},[395,674,598],{"emptyLinePlaceholder":597},[395,676,677],{"class":397,"line":556},[395,678,679],{"class":405},"http\n",[395,681,683,686,689,692,696,699,702,705,707],{"class":397,"line":682},4,[395,684,685],{"class":405},"  .",[395,687,688],{"class":401},"createServer",[395,690,691],{"class":405},"((",[395,693,695],{"class":694},"s9osk","req",[395,697,698],{"class":405},", ",[395,700,701],{"class":694},"res",[395,703,704],{"class":405},") ",[395,706,410],{"class":409},[395,708,709],{"class":405}," {\n",[395,711,713,716,719,722,725],{"class":397,"line":712},5,[395,714,715],{"class":409},"    if",[395,717,718],{"class":405}," (req.url ",[395,720,721],{"class":409},"===",[395,723,724],{"class":419}," \"/events\"",[395,726,727],{"class":405},") {\n",[395,729,731,734,737,739,742],{"class":397,"line":730},6,[395,732,733],{"class":405},"      res.",[395,735,736],{"class":401},"writeHead",[395,738,416],{"class":405},[395,740,741],{"class":426},"200",[395,743,744],{"class":405},", {\n",[395,746,748,751,753,756],{"class":397,"line":747},7,[395,749,750],{"class":419},"        \"Content-Type\"",[395,752,539],{"class":405},[395,754,755],{"class":419},"\"text/event-stream\"",[395,757,758],{"class":405},",\n",[395,760,762,765,767,770],{"class":397,"line":761},8,[395,763,764],{"class":419},"        \"Cache-Control\"",[395,766,539],{"class":405},[395,768,769],{"class":419},"\"no-cache\"",[395,771,758],{"class":405},[395,773,775,778,781],{"class":397,"line":774},9,[395,776,777],{"class":405},"        Connection: ",[395,779,780],{"class":419},"\"keep-alive\"",[395,782,758],{"class":405},[395,784,786],{"class":397,"line":785},10,[395,787,788],{"class":405},"      })\n",[395,790,792],{"class":397,"line":791},11,[395,793,598],{"emptyLinePlaceholder":597},[395,795,797,800,803,806],{"class":397,"line":796},12,[395,798,799],{"class":409},"      let",[395,801,802],{"class":405}," count ",[395,804,805],{"class":409},"=",[395,807,808],{"class":426}," 0\n",[395,810,812,815,818,820,823,825,827],{"class":397,"line":811},13,[395,813,814],{"class":409},"      const",[395,816,817],{"class":426}," timer",[395,819,660],{"class":409},[395,821,822],{"class":401}," setInterval",[395,824,406],{"class":405},[395,826,410],{"class":409},[395,828,709],{"class":405},[395,830,832,835,838,840,843,846,849,852,855,858],{"class":397,"line":831},14,[395,833,834],{"class":405},"        res.",[395,836,837],{"class":401},"write",[395,839,416],{"class":405},[395,841,842],{"class":419},"`data: ${",[395,844,845],{"class":405},"count",[395,847,848],{"class":409},"++",[395,850,851],{"class":419},"}",[395,853,854],{"class":426},"\\n\\n",[395,856,857],{"class":419},"`",[395,859,430],{"class":405},[395,861,863,866,868],{"class":397,"line":862},15,[395,864,865],{"class":405},"      }, ",[395,867,427],{"class":426},[395,869,430],{"class":405},[395,871,873],{"class":397,"line":872},16,[395,874,598],{"emptyLinePlaceholder":597},[395,876,878,881,884,886,889,892,894],{"class":397,"line":877},17,[395,879,880],{"class":405},"      req.",[395,882,883],{"class":401},"on",[395,885,416],{"class":405},[395,887,888],{"class":419},"\"close\"",[395,890,891],{"class":405},", () ",[395,893,410],{"class":409},[395,895,709],{"class":405},[395,897,899,902],{"class":397,"line":898},18,[395,900,901],{"class":401},"        clearInterval",[395,903,904],{"class":405},"(timer)\n",[395,906,908],{"class":397,"line":907},19,[395,909,788],{"class":405},[395,911,913],{"class":397,"line":912},20,[395,914,915],{"class":405},"    }\n",[395,917,919],{"class":397,"line":918},21,[395,920,921],{"class":405},"  })\n",[395,923,925,927,930,932,935],{"class":397,"line":924},22,[395,926,685],{"class":405},[395,928,929],{"class":401},"listen",[395,931,416],{"class":405},[395,933,934],{"class":426},"3000",[395,936,430],{"class":405},[395,938,940],{"class":397,"line":939},23,[395,941,598],{"emptyLinePlaceholder":597},[395,943,945,948,951,953,956],{"class":397,"line":944},24,[395,946,947],{"class":405},"console.",[395,949,950],{"class":401},"log",[395,952,416],{"class":405},[395,954,955],{"class":419},"\"Server running at http://localhost:3000\"",[395,957,430],{"class":405},[367,959,960],{},"啟動伺服器：",[385,962,966],{"className":963,"code":964,"language":965,"meta":390,"style":390},"language-bash shiki shiki-themes github-dark","node server.js\n","bash",[392,967,968],{"__ignoreMap":390},[395,969,970,973],{"class":397,"line":398},[395,971,972],{"class":401},"node",[395,974,975],{"class":419}," server.js\n",[367,977,978,979,982],{},"這時候 ",[392,980,981],{},"/events"," 這個路由會建立一條不關閉的 HTTP 連線，並每秒推送一個數字。",[438,984],{},[362,986,988],{"id":987},"瀏覽器端如何接收-sse","瀏覽器端如何接收 SSE？",[367,990,991,992,995],{},"瀏覽器提供一個原生 API 叫做 ",[392,993,994],{},"EventSource","，專門用來處理 SSE。",[367,997,998],{},"在 HTML 中加入：",[385,1000,1004],{"className":1001,"code":1002,"language":1003,"meta":390,"style":390},"language-html shiki shiki-themes github-dark","\u003Cscript>\n  const eventSource = new EventSource(\"http://localhost:3000/events\")\n\n  eventSource.onmessage = (event) => {\n    console.log(\"received:\", event.data)\n  }\n\n  eventSource.onopen = () => {\n    console.log(\"connection opened\")\n  }\n\n  eventSource.onerror = () => {\n    console.log(\"error occurred\")\n  }\n\u003C/script>\n","html",[392,1005,1006,1017,1040,1044,1066,1081,1086,1090,1106,1119,1123,1127,1142,1155,1159],{"__ignoreMap":390},[395,1007,1008,1011,1014],{"class":397,"line":398},[395,1009,1010],{"class":405},"\u003C",[395,1012,1013],{"class":535},"script",[395,1015,1016],{"class":405},">\n",[395,1018,1019,1022,1025,1027,1030,1033,1035,1038],{"class":397,"line":545},[395,1020,1021],{"class":409},"  const",[395,1023,1024],{"class":426}," eventSource",[395,1026,660],{"class":409},[395,1028,1029],{"class":409}," new",[395,1031,1032],{"class":401}," EventSource",[395,1034,416],{"class":405},[395,1036,1037],{"class":419},"\"http://localhost:3000/events\"",[395,1039,430],{"class":405},[395,1041,1042],{"class":397,"line":556},[395,1043,598],{"emptyLinePlaceholder":597},[395,1045,1046,1049,1052,1054,1057,1060,1062,1064],{"class":397,"line":682},[395,1047,1048],{"class":405},"  eventSource.",[395,1050,1051],{"class":401},"onmessage",[395,1053,660],{"class":409},[395,1055,1056],{"class":405}," (",[395,1058,1059],{"class":694},"event",[395,1061,704],{"class":405},[395,1063,410],{"class":409},[395,1065,709],{"class":405},[395,1067,1068,1071,1073,1075,1078],{"class":397,"line":712},[395,1069,1070],{"class":405},"    console.",[395,1072,950],{"class":401},[395,1074,416],{"class":405},[395,1076,1077],{"class":419},"\"received:\"",[395,1079,1080],{"class":405},", event.data)\n",[395,1082,1083],{"class":397,"line":730},[395,1084,1085],{"class":405},"  }\n",[395,1087,1088],{"class":397,"line":747},[395,1089,598],{"emptyLinePlaceholder":597},[395,1091,1092,1094,1097,1099,1102,1104],{"class":397,"line":761},[395,1093,1048],{"class":405},[395,1095,1096],{"class":401},"onopen",[395,1098,660],{"class":409},[395,1100,1101],{"class":405}," () ",[395,1103,410],{"class":409},[395,1105,709],{"class":405},[395,1107,1108,1110,1112,1114,1117],{"class":397,"line":774},[395,1109,1070],{"class":405},[395,1111,950],{"class":401},[395,1113,416],{"class":405},[395,1115,1116],{"class":419},"\"connection opened\"",[395,1118,430],{"class":405},[395,1120,1121],{"class":397,"line":785},[395,1122,1085],{"class":405},[395,1124,1125],{"class":397,"line":791},[395,1126,598],{"emptyLinePlaceholder":597},[395,1128,1129,1131,1134,1136,1138,1140],{"class":397,"line":796},[395,1130,1048],{"class":405},[395,1132,1133],{"class":401},"onerror",[395,1135,660],{"class":409},[395,1137,1101],{"class":405},[395,1139,410],{"class":409},[395,1141,709],{"class":405},[395,1143,1144,1146,1148,1150,1153],{"class":397,"line":811},[395,1145,1070],{"class":405},[395,1147,950],{"class":401},[395,1149,416],{"class":405},[395,1151,1152],{"class":419},"\"error occurred\"",[395,1154,430],{"class":405},[395,1156,1157],{"class":397,"line":831},[395,1158,1085],{"class":405},[395,1160,1161,1164,1166],{"class":397,"line":862},[395,1162,1163],{"class":405},"\u003C/",[395,1165,1013],{"class":535},[395,1167,1016],{"class":405},[367,1169,1170],{},"重新整理頁面後，就會看到數字持續增加。",[367,1172,1173,1175,1176,1179],{},[392,1174,994],{}," 會",[371,1177,1178],{},"自動重連","，如果網路斷線，瀏覽器會嘗試重新建立連線。這也是 SSE 相較 WebSocket 的一大優勢。",[438,1181],{},[362,1183,1185],{"id":1184},"傳送-json-資料","傳送 JSON 資料",[367,1187,1188],{},"實務上我們通常會傳送 JSON，而不是單純字串。",[367,1190,1191],{},"Server 端可以這樣寫：",[385,1193,1195],{"className":387,"code":1194,"language":389,"meta":390,"style":390},"res.write(`data: ${JSON.stringify({ progress: 50 })}\\n\\n`)\n",[392,1196,1197],{"__ignoreMap":390},[395,1198,1199,1202,1204,1206,1208,1211,1214,1217,1220,1223,1226,1228,1230,1232],{"class":397,"line":398},[395,1200,1201],{"class":405},"res.",[395,1203,837],{"class":401},[395,1205,416],{"class":405},[395,1207,842],{"class":419},[395,1209,1210],{"class":426},"JSON",[395,1212,1213],{"class":419},".",[395,1215,1216],{"class":401},"stringify",[395,1218,1219],{"class":419},"({ progress: ",[395,1221,1222],{"class":426},"50",[395,1224,1225],{"class":419}," })",[395,1227,851],{"class":419},[395,1229,854],{"class":426},[395,1231,857],{"class":419},[395,1233,430],{"class":405},[367,1235,1236],{},"Client 端：",[385,1238,1240],{"className":387,"code":1239,"language":389,"meta":390,"style":390},"eventSource.onmessage = (e) => {\n  const data = JSON.parse(e.data)\n  console.log(data.progress)\n}\n",[392,1241,1242,1262,1282,1292],{"__ignoreMap":390},[395,1243,1244,1247,1249,1251,1253,1256,1258,1260],{"class":397,"line":398},[395,1245,1246],{"class":405},"eventSource.",[395,1248,1051],{"class":401},[395,1250,660],{"class":409},[395,1252,1056],{"class":405},[395,1254,1255],{"class":694},"e",[395,1257,704],{"class":405},[395,1259,410],{"class":409},[395,1261,709],{"class":405},[395,1263,1264,1266,1269,1271,1274,1276,1279],{"class":397,"line":545},[395,1265,1021],{"class":409},[395,1267,1268],{"class":426}," data",[395,1270,660],{"class":409},[395,1272,1273],{"class":426}," JSON",[395,1275,1213],{"class":405},[395,1277,1278],{"class":401},"parse",[395,1280,1281],{"class":405},"(e.data)\n",[395,1283,1284,1287,1289],{"class":397,"line":556},[395,1285,1286],{"class":405},"  console.",[395,1288,950],{"class":401},[395,1290,1291],{"class":405},"(data.progress)\n",[395,1293,1294],{"class":397,"line":682},[395,1295,1296],{"class":405},"}\n",[367,1298,1299],{},"這樣就可以將進度、狀態、log 結構化後傳送到前端。",[438,1301],{},[362,1303,1305],{"id":1304},"sse-與-websocket-的差異","SSE 與 WebSocket 的差異",[367,1307,1308],{},"當選擇即時技術時，很多人會在 SSE 與 WebSocket 之間猶豫。以下是核心差異整理：",[1310,1311,1312,1327],"table",{},[1313,1314,1315],"thead",{},[1316,1317,1318,1323,1325],"tr",{},[1319,1320,1322],"th",{"align":1321},"left","功能",[1319,1324,448],{"align":1321},[1319,1326,485],{"align":1321},[1328,1329,1330,1344,1357,1370,1382],"tbody",{},[1316,1331,1332,1338,1341],{},[1333,1334,1335],"td",{"align":1321},[371,1336,1337],{},"方向",[1333,1339,1340],{"align":1321},"Server → Client (單向)",[1333,1342,1343],{"align":1321},"雙向",[1316,1345,1346,1351,1354],{},[1333,1347,1348],{"align":1321},[371,1349,1350],{},"協定",[1333,1352,1353],{"align":1321},"HTTP",[1333,1355,1356],{"align":1321},"ws / wss",[1316,1358,1359,1364,1367],{},[1333,1360,1361],{"align":1321},[371,1362,1363],{},"複雜度",[1333,1365,1366],{"align":1321},"低",[1333,1368,1369],{"align":1321},"高",[1316,1371,1372,1376,1379],{},[1333,1373,1374],{"align":1321},[371,1375,1178],{},[1333,1377,1378],{"align":1321},"✅ 原生支援",[1333,1380,1381],{"align":1321},"❌ 需自行實作",[1316,1383,1384,1389,1392],{},[1333,1385,1386],{"align":1321},[371,1387,1388],{},"防火牆友善",[1333,1390,1391],{"align":1321},"✅ 非常友善",[1333,1393,1394],{"align":1321},"⚠️ 可能被擋",[367,1396,1397],{},"如果應用情境是 AI 回覆串流、任務進度、log 顯示等「只需要推送資料」的場景，SSE 會比 WebSocket 更簡單且更穩定。",[438,1399],{},[362,1401,1403],{"id":1402},"sse-的限制","SSE 的限制",[367,1405,1406],{},"理解限制同樣重要：",[1408,1409,1410,1416,1422],"ol",{},[475,1411,1412,1415],{},[371,1413,1414],{},"單向傳輸","：SSE 只能 Server → Client。如果需要雙向通訊，例如聊天室，就必須使用 WebSocket。",[475,1417,1418,1421],{},[371,1419,1420],{},"純文字","：SSE 僅支援文字資料。",[475,1423,1424,1427],{},[371,1425,1426],{},"連線限制","：SSE 是長連線模式，要注意伺服器連線數上限。",[438,1429],{},[362,1431,1433],{"id":1432},"在-ai-與即時系統中的實際應用","在 AI 與即時系統中的實際應用",[367,1435,1436],{},"在現代 AI 系統中，SSE 非常常見。例如大型語言模型回覆時會逐字串流（Token Streaming），本質上就是透過類似 SSE 的串流機制實現。",[367,1438,1439],{},"如果你正在開發像 OpenClaw 這類 autonomous coding agent，並希望在瀏覽器中即時看到 VPS 上的 log 輸出，那麼 SSE 會是一個非常自然的選擇。架構可以設計為：",[367,1441,1442],{},[371,1443,1444],{},"Agent → Server log stream → SSE → Browser",[367,1446,1447],{},"這種設計簡單、穩定，且容易部署。",[438,1449],{},[362,1451,1452],{"id":1452},"總結",[367,1454,1455],{},"SSE 是一種建立在 HTTP 之上的單向即時資料推送機制。它透過維持長時間開啟的 HTTP response，讓伺服器可以持續向瀏覽器傳送事件資料。相較於 WebSocket，它更簡單、更穩定，也更適合只需要 Server → Client 推送資料的場景。",[438,1457],{},[362,1459,1460],{"id":1460},"參考資料",[472,1462,1463],{},[475,1464,1465],{},[1466,1467,1471],"a",{"href":1468,"rel":1469},"https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events",[1470],"nofollow","MDN Web Docs – Using Server-Sent Events",[1473,1474,1475],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":390,"searchDepth":545,"depth":545,"links":1477},[1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489],{"id":364,"depth":545,"text":365},{"id":442,"depth":545,"text":443},{"id":516,"depth":545,"text":517},{"id":576,"depth":545,"text":577},{"id":633,"depth":545,"text":634},{"id":987,"depth":545,"text":988},{"id":1184,"depth":545,"text":1185},{"id":1304,"depth":545,"text":1305},{"id":1402,"depth":545,"text":1403},{"id":1432,"depth":545,"text":1433},{"id":1452,"depth":545,"text":1452},{"id":1460,"depth":545,"text":1460},"從原理到實戰，完整理解 SSE 的運作機制、資料格式與 Node.js 實作方式，並學會何時選擇 SSE 而非 WebSocket。","md",null,{"tags":1494,"category":203,"date":1500},[1495,1496,1497,1498,1499],"sse","http","nodejs","realtime","backend","2026-02-12",{"title":305,"description":1490},"RqEL458tlStn8NfCU419Zj2WUlsx_bLCWr5xvl2yit8",[1504,1506],{"title":301,"path":302,"stem":303,"description":1505,"children":-1},"從 REST constraints 出發，理解 RESTful API 為何這樣設計，以及每個原則在實務開發中的意義與常見錯誤。",{"title":309,"path":310,"stem":311,"description":1507,"children":-1},"深入理解 WebSocket 的核心概念，並透過 Node.js 與 ws 套件實作即時雙向通訊。",1776690847906]