Now.js Framework Documentation

Now.js Framework Documentation

TaskMonitorComponent

TH 04 Sep 2026 01:40

TaskMonitorComponent

ภาพรวม

TaskMonitorComponent เฝ้างานที่เซิร์ฟเวอร์รันอยู่เบื้องหลัง แล้วรายงานเมื่องานจบ

มันเกิดมาเพื่องานที่ อายุยาวกว่าคำขอที่สั่งมัน — งานฝั่งเซิร์ฟเวอร์ที่วัดกันเป็นนาที
เช่น บีบอัดไฟล์ นำเข้าข้อมูล สร้างรายงาน หรือออกใบรับรอง ทำให้เสร็จภายในคำขอที่สั่ง
ไม่ได้ เซิร์ฟเวอร์จึงเริ่มงานไว้เบื้องหลังแล้วตอบ 202 Accepted กลับมาทันที

จากจุดนั้นเป็นต้นไป หน้าที่กดปุ่มไม่ใช่หน้าที่เห็นผล — แท็บถูกปิด อีกคนเปิดหน้าเดียวกัน
หรือเบราว์เซอร์ถูกทิ้งไว้เป็นชั่วโมง · คำตอบจึงอยู่ในแท็บเบราว์เซอร์ไม่ได้ คอมโพเนนต์นี้
ไปถามเซิร์ฟเวอร์เป็นระยะแทน แล้วแสดงสิ่งที่เซิร์ฟเวอร์ตอบ

ใช้เมื่อไร:

  • มีปุ่มที่สั่งงานแล้วเซิร์ฟเวอร์ไปทำต่อทีหลัง
  • หน้าจอต้องบอกว่า "ยังทำอยู่" ได้แม้โหลดหน้าใหม่ หรือเปิดจากคนอื่น
  • มีอะไรบนหน้าที่ต้องตอบสนองเมื่องานจบ

ทำไมต้องใช้:

  • ✅ ไม่ฝังรูปร่าง API ไว้เลย — ทุกฟิลด์แมปผ่าน data attribute
  • ✅ ถามถี่เฉพาะตอนมีงานทำ แล้วผ่อนเป็นจังหวะว่างเมื่อไม่มี
  • ✅ หยุดถามสนิทเมื่อแท็บถูกซ่อน
  • ✅ ถอยจังหวะเมื่อเกิดข้อผิดพลาด แทนที่จะกระหน่ำเซิร์ฟเวอร์ที่กำลังมีปัญหา
  • ✅ ยิง DOM event เมื่อแต่ละงานจบ อะไรบนหน้าก็ตอบสนองได้
  • ✅ escape ทุกค่าที่เรนเดอร์

การใช้งานพื้นฐาน

แบบ HTML

<div data-component="task-monitor"
     data-endpoint="/api/v2/jobs"
     data-items="data.active"
     data-label-field="label"
     data-status-field="status"></div>

มาร์กอัปข้างบนคาดหวังคำตอบหน้าตาแบบนี้:

{
  "ok": true,
  "data": {
    "active": [
      {"id": 12, "label": "สำรอง example.com", "status": "running", "message": ""}
    ]
  }
}

รูปร่างนั้นไม่ได้บังคับ · data-items คือ dot path ไปยังที่ที่อาร์เรย์อยู่ (ใส่ ''
แปลว่าตัว body เป็นอาร์เรย์เอง) ส่วน *-field ทั้งสี่บอกชื่อคีย์ภายในงานหนึ่งชิ้น

แบบ JavaScript

const monitor = TaskMonitorComponent.create(document.querySelector('#tasks'), {
  endpoint: '/api/tasks',
  interval: 2000,
  onFinish: (task) => console.log('เสร็จแล้ว:', task.label)
});

// หลังสั่งงานใหม่ ให้ถามทันทีโดยไม่ต้องรอครบจังหวะ
TaskMonitorComponent.refresh(monitor);

ตัวเลือก

ทุกตัวเลือกตั้งเป็น data-* ได้ โดยเขียนแบบ kebab-case: idleInterval คือ
data-idle-interval

ตัวเลือก ค่าเริ่มต้น ความหมาย
endpoint '' จำเป็น · URL ที่จะไปถาม
items 'data.active' dot path ไปยังอาร์เรย์ของงาน · '' = ตัว body เอง
idField 'id' คีย์ที่ใช้ระบุว่างานชิ้นเดียวกันข้ามรอบการถาม
labelField 'label' คีย์ของชื่อที่จะแสดง
statusField 'status' คีย์ของสถานะ
messageField 'message' คีย์ของข้อความผลลัพธ์
progressField 'progress' คีย์ของค่า 0–100 · ไม่มี = ไม่แสดงแถบความคืบหน้า
runningValues 'queued,running,pending,active' ค่าสถานะที่แปลว่า "ยังทำอยู่"
failedValues 'failed,error,lost,cancelled' ค่าสถานะที่แปลว่าจบแบบไม่สำเร็จ
interval 3000 จังหวะถามขณะมีงานทำ (มิลลิวินาที)
idleInterval 30000 จังหวะถามขณะไม่มีงาน · 0 = หยุดถาม
maxErrors 0 ล้มติดกันกี่ครั้งแล้วเลิก · 0 = ไม่เลิก
maxInterval 120000 เพดานของการถอยจังหวะเมื่อผิดพลาด
autoRender true false = ให้ callback เรนเดอร์เอง
emptyText '' ข้อความตอนไม่มีงาน · ว่าง = ไม่เรนเดอร์อะไร
hideWhenIdle true ใส่ hidden ให้ element ขณะไม่มีงาน
notify true แสดง toast เมื่อแต่ละงานจบ ถ้ามี NotificationManager

Callback

Callback รูปแบบ เรียกเมื่อ
onInit (instance) หลังถามสำเร็จครั้งแรก
onUpdate (tasks, instance) ทุกครั้งที่ถามสำเร็จ
onFinish (task, instance) ครั้งเดียวต่องาน เมื่องานนั้นหยุดทำงาน
onError (error, instance) เมื่อถามไม่สำเร็จ
onRender (tasks, instance) แทนการเรนเดอร์ในตัว
onDestroy (instance) เมื่ออินสแตนซ์ถูกทำลาย

เมธอด

เมธอด คำอธิบาย
init(options?) ติดตั้งให้ทุก element ที่ตรง · ถูกเรียกเองเมื่อ DOM พร้อม
create(element, options?) สร้างตัวเฝ้าหนึ่งตัว · คืนอินสแตนซ์ หรือ null
refresh(instanceOrElement) ถามเดี๋ยวนี้ ไม่สนใจตารางเวลา
destroy(instanceOrElement) หยุดถามและลืมอินสแตนซ์นั้น

เหตุการณ์

task-monitor:finished

ยิงครั้งเดียวต่องาน เมื่องานนั้นหยุดทำงาน · เป็น bubbling event ผู้ฟังที่ระดับไหน
เหนือขึ้นไปก็รับได้

document.addEventListener('task-monitor:finished', (event) => {
  const {task, label, message, failed} = event.detail;

  if (!failed) TableManager.refresh('backups');
});

เป็น DOM event ไม่ใช่แค่ callback เพราะหน้าเว็บต้องตอบสนองได้โดยไม่ต้องถืออินสแตนซ์
ไว้ — จะรีเฟรชตาราง ปิดกล่องโต้ตอบ หรือเปิดปุ่มที่อยู่คนละมุมของหน้าจอก็ได้

ตัดสินว่า "จบแล้ว" อย่างไร

คอมโพเนนต์จำ id ของงานที่กำลังทำอยู่ในรอบก่อนหน้า · งานที่เคยอยู่ในชุดนั้นแล้วตอนนี้
ไม่ได้ทำงานแล้ว = จบ ไม่ว่าเซิร์ฟเวอร์จะรายงานว่าจบ หรือเอามันออกจากรายการไปเลย ·
สองอย่างนั้นมีความหมายเดียวกันสำหรับคนที่กำลังรอดูอยู่ และถ้าไม่จำไว้ งานที่หายไปเฉย ๆ
จะแยกไม่ออกจากงานที่ไม่เคยมีอยู่

runningValues เป็นชุดปิดโดยตั้งใจ · สถานะที่ไม่มีใครคาดไว้ — เซิร์ฟเวอร์รุ่นใหม่เพิ่ม
cancelled มา หรือพิมพ์ผิด — จะถูกนับว่าจบและหยุดถาม ดีกว่าปล่อยให้หน้าเว็บถามไป
เรื่อย ๆ เพื่อสิ่งที่จบไปแล้ว

พฤติกรรมการถาม

สามอย่างที่กันไม่ให้กลายเป็นภาระของเซิร์ฟเวอร์:

  1. ผ่อนเมื่อว่าง — ไม่มีงานทำแล้วจังหวะจะลดลงเป็น idleInterval หรือหยุดสนิทที่ 0
  2. แท็บที่ถูกซ่อนหยุดสนิท — แท็บพื้นหลังไม่ถามเลยสักครั้ง และกลับมาดูเมื่อไรก็ถามทันที
    หน้าจอจึงไม่เคยเก่ากว่าเวลาที่ใช้ตอบหนึ่งคำขอ
  3. ถอยจังหวะเมื่อพลาด — ล้มติดกันแต่ละครั้งเพิ่มเวลารอเป็นสองเท่าจนถึง maxInterval ·
    ถ้าไม่มีข้อนี้ ตัวเฝ้าที่เปิดค้างไว้ตอนเซิร์ฟเวอร์กำลังรีสตาร์ตจะยิงคำขอทุกไม่กี่วินาที
    ตราบเท่าที่แท็บยังเปิดอยู่ ซึ่งคือรูปร่างของการ DoS ตัวเองใส่เครื่องที่กำลังมีปัญหาอยู่แล้ว

การจัดสไตล์

คอมโพเนนต์เรนเดอร์คลาสของตัวเอง และมาพร้อม Now/css/task-monitor.css:

<ul class="task-monitor-list">
  <li class="task-monitor-item is-running">
    <span class="task-monitor-status">running</span>
    <span class="task-monitor-label">สำรอง example.com</span>
    <progress class="task-monitor-progress" max="100" value="40"></progress>
    <span class="task-monitor-message">…</span>
  </li>
</ul>

สถานะคือ is-running / is-done / is-failed — เป็น modifier ของคอมโพเนนต์เอง
ไม่ยืมคลาสจากสไตล์ของแอปพลิเคชันใด · คอมโพเนนต์ของเฟรมเวิร์กที่เรนเดอร์ pill-danger
จะดูถูกต้องเฉพาะในโปรเจกต์เดียวที่บังเอิญนิยามคลาสนั้นไว้

ถ้าต้องการหน้าตาคนละแบบไปเลย ใช้ onRender แล้วไม่ต้องสนใจทั้งหมดนี้

ความปลอดภัย

ทุกอย่างที่เรนเดอร์มาจากเซิร์ฟเวอร์ และชื่องานมักเป็นชื่อไฟล์หรือชื่อโดเมน ที่คนอื่นตั้ง
ซึ่งเป็นค่าที่ลูกค้าควบคุมได้พอดี · ทุกสตริงถูก escape ก่อนถึง innerHTML เพราะถ้าเรนเดอร์
ดิบ ๆ ค่าใดค่าหนึ่งในนั้นคือ stored XSS บนหน้าจอของผู้ดูแลเอง

onRender แทนการเรนเดอร์ในตัวทั้งหมด ผู้เขียนตัวเรนเดอร์เองจึงต้องรับผิดชอบการ escape เอง

สิ่งที่พึ่งพา

สิ่งที่พึ่งพา จำเป็นไหม ใช้ทำอะไร
CoreObserver ไม่ ติดตั้งให้ element ที่เพิ่มมาทีหลัง · ถ้าไม่มีจะใช้ MutationObserver ของตัวเอง
window.http ไม่ ยิงคำขอพร้อม session และ CSRF ที่แอปใช้อยู่ · ถ้าไม่มีจะใช้ fetch
NotificationManager ไม่ toast ที่แสดงเมื่องานจบ

การทดสอบ

tests/task-monitor.mjs รันบันเดิลจริงในเบราว์เซอร์จริงกับเซิร์ฟเวอร์ HTTP จริง —
18 ข้อ ครอบคลุมการติดตั้ง เหตุการณ์ตอนจบ สถานะที่ไม่รู้จัก การ escape การถอยจังหวะ
เมื่อผิดพลาด และการเก็บกวาดตัวจับเวลา

cd tests && node task-monitor.mjs