Service Worker ในภาษา JavaScript

Service Worker ในภาษา JavaScript

บทความนี้อธิบายแนวคิดของ Service Worker ในภาษา JavaScript

เราจะอธิบายทีละขั้นตอนตั้งแต่พื้นฐานของ Service Worker ไปจนถึงการควบคุมแคชอย่างเป็นรูปธรรม

YouTube Video

offline.html
 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4  <meta charset="UTF-8">
 5  <title>Offline</title>
 6</head>
 7<body>
 8  <h1>You are offline</h1>
 9  <p>This is the offline fallback page.</p>
10</body>
11</html>
style.css
1body {
2  font-family: sans-serif;
3  background-color: #f0f0f0;
4  padding: 20px;
5}
6h1 {
7  color: #333;
8}
javascript-service-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <div class="container">
111        <h2>HTML Sample</h2>
112        <button id="fetchBtn">Fetch Test</button>
113    </div>
114
115    <script>
116        // Override console.log to display messages in the #output element
117        (function () {
118            // Override console.log
119            const originalLog = console.log;
120            console.log = function (...args) {
121                originalLog.apply(console, args);
122                const message = document.createElement('div');
123                message.textContent = args.map(String).join(' ');
124                output.appendChild(message);
125            };
126
127            // Override console.error
128            const originalError = console.error;
129            console.error = function (...args) {
130                originalError.apply(console, args);
131                const message = document.createElement('div');
132                message.textContent = args.map(String).join(' ');
133                message.style.color = 'red'; // Color error messages red
134                output.appendChild(message);
135            };
136        })();
137
138        document.getElementById('executeBtn').addEventListener('click', () => {
139            // Prevent multiple loads
140            if (document.getElementById('externalScript')) return;
141
142            const script = document.createElement('script');
143            script.src = 'javascript-service-worker.js';
144            script.id = 'externalScript';
145            //script.onload = () => console.log('javascript-service-worker.js loaded and executed.');
146            //script.onerror = () => console.log('Failed to load javascript-service-worker.js.');
147            document.body.appendChild(script);
148        });
149    </script>
150</body>
151</html>

Service Worker ในภาษา JavaScript

Service Worker เป็นฟีเจอร์ในภาษา JavaScript ที่อยู่ระหว่างเบราว์เซอร์และเครือข่าย ช่วยให้สามารถแคชคำขอและรองรับการทำงานแบบออฟไลน์ มันเป็นเทคโนโลยีหลักของ PWA (Progressive Web Apps) และมอบประสบการณ์เหมือนแอปเนทีฟให้กับเว็บแอปพลิเคชัน

Service Worker คืออะไร?

Service Worker เป็นไฟล์ JavaScript ที่ทำงานอยู่ใน background thread ของเบราว์เซอร์ มันทำงานในเธรดที่แยกจากหน้าหลัก ไม่สามารถเข้าถึง UI ได้ แต่สามารถสกัดกั้นคำขอเครือข่าย จัดการแคช และจัดการ push notification ได้

คุณสมบัติหลักของ Service Worker มีดังนี้:

  • จะทำงานผ่าน HTTPS เท่านั้น ยกเว้นบน localhost
  • ใช้ API แบบอะซิงโครนัสที่อิงตาม Promise
  • ทำงานโดยขับเคลื่อนด้วยเหตุการณ์ เช่น install, activate, fetch, และ push

การลงทะเบียน Service Worker

ก่อนอื่น มาเขียนโค้ดเพื่อสมัครใช้งาน Service Worker ในเบราว์เซอร์กัน

 1if ('serviceWorker' in navigator) {
 2    window.addEventListener('load', () => {
 3        navigator.serviceWorker.register('/sw.js')
 4        .then(registration => {
 5            console.log(
 6                'Service Worker registered with scope:',
 7                registration.scope
 8            );
 9        })
10        .catch(error => {
11            console.error('Service Worker registration failed:', error);
12        });
13    });
14}

คำอธิบาย

  • ใช้ navigator.serviceWorker.register() เพื่อลงทะเบียนไฟล์ /sw.js (ไฟล์ Service Worker)
  • สามารถใช้ then จัดการกรณีสำเร็จ และ catch จัดการข้อผิดพลาดระหว่างการลงทะเบียน
  • registration.scope หมายถึงช่วงของเส้นทาง (ขอบเขต) ที่ได้รับผลกระทบจาก Service Worker
  • โดยปกติ ขอบเขตจะเป็นไดเรกทอรีที่ไฟล์ที่ลงทะเบียนไว้ (ในกรณีนี้คือ /sw.js) อยู่รวมถึงไดเรกทอรีย่อยต่าง ๆ

ขอบเขตของ Service Worker

หากต้องการจำกัดขอบเขต คุณสามารถระบุ scope โดยใช้พารามิเตอร์ที่สองของ register

1navigator.serviceWorker.register('/sw.js', { scope: '/app/' })
2.then(registration => {
3    console.log(
4        'Service Worker registered with scope:',
5        registration.scope
6    );
7});

คำอธิบาย

  • ด้วยการตั้งค่านี้ เฉพาะหน้าเว็บที่อยู่ภายใต้ /app/ เท่านั้นที่จะถูกควบคุมโดย Service Worker

การสร้างไฟล์ Service Worker

ต่อไป ให้สร้างไฟล์ชื่อ sw.js และเขียนฟังก์ชันเหตุการณ์พื้นฐาน

1// sw.js
2const CACHE_NAME = 'my-cache-v1';
3const urlsToCache = [
4    '/',
5    '/index.html',
6    '/styles.css',
7    '/script.js',
8    '/offline.html'
9];

โค้ดนี้กำหนดรายการทรัพยากรที่จะถูกเก็บแคช

บทบาทและกลไกของแต่ละเหตุการณ์

install

 1// Install event (initial caching)
 2self.addEventListener('install', event => {
 3    console.log('[ServiceWorker] Install');
 4    event.waitUntil(
 5        caches.open(CACHE_NAME).then(cache => {
 6            console.log('[ServiceWorker] Caching app shell');
 7            return cache.addAll(urlsToCache);
 8        })
 9    );
10});
  • ฟังก์ชัน self.addEventListener('install') จะถูกเรียกใช้เมื่อ Service Worker ถูกลงทะเบียนครั้งแรก ในขั้นตอนนี้ ไฟล์ที่จำเป็นจะถูกเก็บไว้ในแคชล่วงหน้า

activate

 1// Activation event (delete old caches)
 2self.addEventListener('activate', event => {
 3    console.log('[ServiceWorker] Activate');
 4    event.waitUntil(
 5        caches.keys().then(keyList => {
 6            return Promise.all(keyList.map(key => {
 7                if (key !== CACHE_NAME) {
 8                    console.log('[ServiceWorker] Removing old cache:', key);
 9                    return caches.delete(key);
10                }
11            }));
12        })
13    );
14    return self.clients.claim();
15});
  • ในเหตุการณ์ activate แคชเก่าจะถูกลบเพื่อปรับปรุงพื้นที่จัดเก็บ จะเก็บไว้เฉพาะแคชของเวอร์ชันใหม่เท่านั้น

fetch

1// Fetch event (cache-first strategy)
2self.addEventListener('fetch', event => {
3  console.log('[ServiceWorker] Fetch', event.request.url);
4    event.respondWith(
5        caches.match(event.request).then(response => {
6            return response || fetch(event.request).catch(() => caches.match('/offline.html'));
7        })
8    );
9});

คำขอ HTTP ทั้งหมดจะถูกสกัดกั้น—ถ้ามีเวอร์ชันที่ถูกแคชไว้แล้วก็จะตอบกลับจากแคช มิฉะนั้นจะดึงข้อมูลจากเครือข่าย เมื่อออฟไลน์ จะส่งคืนหน้าทดแทน (เช่น offline.html)

กำลังยืนยันการดำเนินการ

ลองตรวจสอบการทำงานของ Service Worker ในทางปฏิบัติกันเถอะ

 1document.getElementById('fetchBtn').addEventListener('click', () => {
 2    fetch('/style.css')
 3        .then(response => response.text())
 4        .then(data => {
 5            console.log('Fetched data:', data);
 6        })
 7        .catch(error => {
 8            console.error('Fetch failed:', error);
 9        });
10});
  • ที่นี่ เราจะตรวจสอบการลงทะเบียนของ Service Worker และพฤติกรรมการดึงข้อมูลทรัพยากรโดยการคลิกปุ่มทดสอบ

ตัวอย่างกลยุทธ์การแคช

ต่อไปนี้คือตัวอย่างกลยุทธ์การแคชที่ใช้บ่อย:

Cache First (แคชก่อน)

นี่คือตัวอย่างการใช้กลยุทธ์ Cache First:

1self.addEventListener('fetch', event => {
2    event.respondWith(
3        caches.match(event.request).then(response => {
4            return response || fetch(event.request);
5        })
6    );
7});
  • โค้ดนี้ใช้กลยุทธ์ cache-first โดยจะคืนทรัพยากรที่ร้องขอจากแคชหากมีอยู่แล้ว; หากไม่มี จะดึงข้อมูลจากเครือข่าย

Network First (เน็ตเวิร์คก่อน)

นี่คือตัวอย่างการใช้กลยุทธ์ Network First:

 1self.addEventListener('fetch', event => {
 2    event.respondWith(
 3        fetch(event.request)
 4            .then(response => {
 5                return caches.open(CACHE_NAME).then(cache => {
 6                    cache.put(event.request, response.clone());
 7                    return response;
 8                });
 9            })
10            .catch(() => caches.match(event.request))
11    );
12});
  • โค้ดนี้ใช้กลยุทธ์ network-first โดยจะดึงทรัพยากรที่ร้องขอจากเครือข่ายก่อน และหากล้มเหลว จึงจะดึงข้อมูลจากแคช

แคชเฉพาะไฟล์สไตล์และ JavaScript เรียกใช้งาน API แบบเรียลไทม์

นี่คือตัวอย่างการใช้งานที่แคชไฟล์สไตล์และ JavaScript ในขณะที่ดึงข้อมูล API แบบเรียลไทม์:

 1self.addEventListener('fetch', event => {
 2    if (event.request.url.includes('/api/')) {
 3        // Fetch API responses in real-time without caching
 4        return;
 5    }
 6
 7    // Use cache-first strategy for static files
 8    event.respondWith(
 9        caches.match(event.request).then(response => {
10            return response || fetch(event.request);
11        })
12    );
13});
  • โค้ดนี้เข้าถึง API แบบเรียลไทม์อยู่เสมอ และใช้กลยุทธ์ cache-first กับไฟล์คงที่ เช่น stylesheets และ JavaScript

กระบวนการอัปเดต

กระบวนการอัปเดตของ Service Worker มีดังนี้:

  1. sw.js ใหม่ถูกตรวจพบ
  2. เหตุการณ์ install จะถูกเรียกใช้
  3. รอจนกว่า Service Worker ก่อนหน้าจะว่าง
  4. เหตุการณ์ activate จะถูกเรียกใช้
  5. จะเปลี่ยนไปใช้ Service Worker อันใหม่
  6. มีการเรียกใช้งานเหตุการณ์ controllerchange

การตรวจจับการอัปเดต

เมื่อ Service Worker ได้รับการติดตั้งแล้ว อันเก่าจะยังคงถูกใช้งานอยู่ จนกว่าจะมีการเข้าชมครั้งถัดไป เพื่อให้อัปเดตมีผล มักจะใช้โค้ดที่ตรวจจับการอัปเดตและรีโหลดหน้าเว็บ

1navigator.serviceWorker.addEventListener('controllerchange', () => {
2    window.location.reload();
3});
  • เหตุการณ์ controllerchange จะถูกเรียกใช้เมื่อคอนโทรลเลอร์ของ Service Worker ซึ่งก็คือ Service Worker ที่ควบคุมหน้าปัจจุบัน มีการเปลี่ยนแปลง
  • หน้าที่เปิดอยู่ก่อนหน้านี้จะยังคงใช้ Service Worker ปัจจุบัน และ Service Worker ที่ติดตั้งใหม่จะยังไม่มีผลต่อหน้านั้นในทันที ดังนั้นจึงมีเทคนิคที่ใช้เหตุการณ์ controllerchange เพื่อตรวจจับว่า controller ใหม่ทำงานแล้ว จากนั้นจะรีโหลดหน้าเว็บเพื่อใช้งานอัปเดตทันที

ข้อควรระวังและแนวปฏิบัติที่ดีที่สุด

เมื่อใช้ Service Worker ควรคำนึงถึงประเด็นต่อไปนี้:

  • ต้องใช้ HTTPS เนื่องจากข้อจำกัดด้านความปลอดภัย จะไม่ทำงานผ่าน http:// ยกเว้นบน localhost

  • ชื่อไฟล์แบบแฮช ชื่อแคชสามารถประกอบด้วยชื่อไฟล์, URL และข้อมูลเวอร์ชัน

  • การสื่อสารกับไคลเอนต์ ใช้ postMessage ในการสื่อสารระหว่าง Service Worker กับ JavaScript ของหน้าเว็บ

สรุป

Service Worker เป็นเทคโนโลยีสำคัญสำหรับการรองรับออฟไลน์และปรับปรุงประสิทธิภาพของเว็บแอป ด้วยความเข้าใจในกระบวนการติดตั้ง การเปิดใช้งาน และการจัดการ fetch พร้อมกับการนำกลยุทธ์การแคชที่เหมาะสมมาใช้ คุณจะสามารถสร้างเว็บแอปพลิเคชันที่มีคุณภาพสูงขึ้นได้

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video