The Service Worker
essentially acts as a proxy server between a web application, browser, and the network (when available). This API is designed to create efficient offline experiences by intercepting network requests and taking appropriate actions based on the availability of the network. It can update resources from the server, provide entry points for push notifications, and access background sync APIs.
The Service Worker
is essentially used for caching resources in the browser. However, it goes beyond just caching and further optimizes through the use of workers. It is based on the Web Worker
of HTML5, which means it does not block the execution of the current JavaScript thread. Its main working principles are as follows:
- It runs as a background thread independent of the current web page thread.
- It acts as a network proxy, intercepting requests made by the web page and returning cached files.
In simple terms, the Service Worker
is a background worker thread that runs continuously, serving as a service. It is well-suited for functionalities that do not require independent resource data or user interaction. The most common use case is intercepting and handling network requests. Here are some key points to note:
- It is based on the
Web Worker
, which is an independent thread separate from the JavaScript main thread, allowing resource-intensive operations to be executed without blocking the main thread. - It adds the ability for offline caching on top of the
Web Worker
. - It acts as a proxy server between the web application (server) and the browser, intercepting requests made by the web page and taking actions specified by the developer.
- It creates efficient offline experiences by caching infrequently updated content in the browser, improving the browsing experience.
- It is event-driven and has a lifecycle.
- It can access the cache and indexDB.
- It supports push notifications.
- It allows developers to have control over managing the content and versions of the cache.
The Service Worker
has other use cases as well, and the standard of Service Worker
can be used to make web platforms more similar to native applications. Some of these use cases include:
- Background data synchronization.
- Responding to resource requests from other sources.
- Centralized receiving of computationally expensive data updates, such as geolocation and gyroscope information, allowing multiple pages to utilize the same set of data.
- Compiling and managing dependencies of CoffeeScript, LESS, CJS/AMD modules, etc. on the client side (for development purposes).
- Background service hooks.
- Custom templates for specific URL patterns. This enhances performance by prefetching resources that the user may need, such as the next few images in an album.
- It can be combined with App Manifest and Service Worker to implement features like Progressive Web Apps (PWA) installation and offline functionality.
- Background sync, where a service worker can update the cache even if a specific site is not being visited by the user.
- Push notifications, where a service worker can send a notification to the user when new content is available.
- Responding to time or date changes.
- Entering a geofence (an application of Location-Based Services).
Here is an example of a simple Service Worker
application that can still be used when there is no internet connection. The relevant code can be found at https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker
. In this example, a simple native Service Worker
is implemented without using any libraries like Workbox
. However, it is worth noting that writing a native Service Worker
can be cumbersome and complex, so using libraries like Workbox
can simplify the process. Before using a Service Worker
, there are some important considerations:
Service Worker
runs on a worker, which means it cannot access the DOM.- It is designed to be completely asynchronous, and synchronous APIs like XHR and localStorage cannot be used in a service worker.
- For security reasons,
Service Workers
can only be hosted on HTTPS, althoughhttp
can be used for local debugging onlocalhost
. - In Firefox browser's private browsing mode,
Service Worker
is not available. - Its lifecycle is independent of the page (it can continue running even if the associated page is closed, and it can start even without an associated page).
First, use Node
to start a basic web
server. You can use the anywhere
package, or any other server of your choice. After executing the command, you can access http://localhost:7890/
. Additionally, it is recommended to restart the server after writing the relevant code. I encountered issues with caching before, including disk cache
and memory cache
, and they were resolved after restarting the server. Also, please note that the link to be opened is localhost
, as the browser may not automatically open to localhost
. If you want to clear the cache, you can click on Clear site data
in the Storage
section of the browser console under the Application
tab. This will clear all the cache on the website. If you are using a server environment such as express
or koa
, you can also try using Service Worker
to cache data requests. Simply provide the path for the data request.
$ npm install -g anywhere
$ anywhere 7890 # http://localhost:7890/
Create an index.html
file and an sw.js
file, and import the relevant resource files. The directory structure is as follows, and you can refer to https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker
. Of course, you can directly clone and run a static file server to use it directly.
simple--service-worker
├── static
│ ├── avatar.png
│ └── cache.js
├── index.html
└── sw.js
Simply import the relevant files in the html
file, mainly to leverage the browser environment, and focus on the js
section.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Service Worker</title>
<style type="text/css">
.avatar{
width: 50px;
height: 50px;
border-radius: 50px;
}
</style>
</head>
<body>
<img class="avatar" src="./static/avatar.png">
<script type="text/javascript">
navigator.serviceWorker
.register("sw.js")
.then(() => {
console.info("Registration successful");
})
.catch(() => {
console.error("Registration failed");
});
</script>
<script src="./static/cache.js"></script>
</body>
</html>
The first step in using Service Worker
is to tell the browser to register a Service Worker
script. In this case, we directly write it in the index.html
file. By default, Service Worker
only works for the root directory /
. If you want to change the scope, you can add a second parameter { scope: "/xxx"}
when registering, or directly specify the path /xxx/sw.js
during registration.
navigator.serviceWorker
.register("sw.js")
.then(() => {
console.info("Registration successful");
}).catch(err => {
console.error("Registration failed");
})
Once the registration is successful, the work of the Service worker
script begins. The following code is written inside the service worker
script. After registration, the install
event will be triggered, and the service worker
script needs to listen for this event. First, the name of the cache
is defined, which serves as the key to identify this cache object. The urlsToCache
array contains the data that will be cached. As long as the relevant path
is provided, even data requests can be cached, not just resource files. However, this can only be done for Get
requests, as determined by the Cache
API. After that, the install
process is carried out. The event.waitUntil
can be understood as the function of new Promise
, which means that it waits for the serviceWorker
to start running before continuing with the subsequent code. The actual parameter it accepts can only be a Promise
. According to the explanation on MDN, it is because some time is needed for oninstall
and onactivate
to complete. The service worker
standard provides a waitUntil
method, which is called when oninstall
or onactivate
is triggered. It accepts a promise
, and until this promise
is successfully resolved, functional events will not be dispatched to the service worker
. After that, the cache
identified by the key
of CACHE_NAME
is retrieved from caches
, and then the path
in the array is added to the cache
using cache.addAll
. When the page is first opened, the Service worker
will automatically request the relevant data and cache it. The data requested using the Service worker
will be displayed with a small gear icon in the Network
tab of the Chrome console, making it easy to recognize.
const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];
this.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log("[Service Worker]", urlsToCache);
return cache.addAll(urlsToCache);
})
);
});
Next is the activated
phase. If it is the first time loading the sw
, after installation, it will directly enter the activated
phase. However, if the sw
is being updated, the situation becomes more complicated. The process is as follows: First, the old sw
is A
, and the new sw
version is B
. B
enters the install
phase while A
is still in working state, so B
enters the waiting
phase. Only when A
is terminated can B
replace A
and start working normally. There are several ways to trigger the terminated
state: 1) Closing the browser for a period of time. 2) Manually clearing the Service Worker. 3) Skipping the waiting
phase during sw
installation. After that, the activated
phase is entered, and the sw
starts working. In the activated
phase, many meaningful things can be done, such as updating the key
and value
stored in the Cache
. In the code below, any CACHE_NAME
that is not in the whitelist will be cleared. A version control can also be implemented here, where previous versions need to be cleared. Additionally, the current related cache is checked.
this.addEventListener("activate", event => {
// Clean up `CACHE_NAME` that is not in the whitelist
const cacheWhitelist = ["service-worker-demo"];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
// Check the cache
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
);
});
Afterwards, it is the stage of intercepting requests. This stage is a crucial stage for the sw
, used to intercept and proxy all specified requests, and perform corresponding operations. All caching operations are done in this stage. First, we intercept all requests. The initial conditional statement is used to prevent all requests from being intercepted and made within the worker. However, it can also be used without the conditional statement. Then, if a request matches a cache, the data is directly retrieved from the cache; otherwise, a new request is made using fetch
. Additionally, if needed, we can cache all requests made without matching during the event response.
this.addEventListener("fetch", event => {
const url = new URL(event.request.url);
if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
event.respondWith(
caches.match(event.request).then(resp => {
if (resp) {
console.log("fetch ", event.request.url, "has cache, retrieving from cache");
return resp;
} else {
console.log("fetch ", event.request.url, "has no cache, retrieving from network");
return fetch(event.request);
// // If necessary, we don't need to match when the event is triggered, we can directly cache all requests made
// return fetch(event.request).then(response => {
// return caches.open(CACHE_NAME).then(cache => {
// cache.put(event.request, response.clone());
// return response;
// });
// });
}
})
);
}
});
Console output when opened for the first time:
cache.js loaded
[Service Worker] (3) ['/', '/static/avatar.png', '/static/cache.js']
Registration successful
(3) [Request, Request, Request]
Console output when opened for the second time and onwards:
fetch http://localhost:7811/static/avatar.png has cache, retrieving from cache
fetch http://localhost:7811/static/cache.js has cache, retrieving from cache
Registration successful
cache.js loaded
With this, we have completed a simple example. When the page is opened for the second time, we can disconnect the browser's network connection, such as by closing the file server or selecting Offline
in the Network
tab of the console. We can see that the page still loads normally without requiring a network service. Additionally, in the Size
column of the relevant data in the Network
tab, there will be a (ServiceWorker)
indication, indicating that the resources are loaded from the cache data of the ServiceWorker
. You can clone this example from https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker
and run it.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Service Worker</title>
<style type="text/css">
.avatar{
width: 50px;
height: 50px;
border-radius: 50px;
}
</style>
</head>
<body>
<img class="avatar" src="./static/avatar.png">
<script type="text/javascript">
navigator.serviceWorker
.register("sw.js")
.then(() => {
console.info("Registration successful");
})
.catch(() => {
console.error("Registration failed");
});
</script>
<script src="./static/cache.js"></script>
</body>
</html>
// sw.js
const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];
this.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log("[Service Worker]", urlsToCache);
return cache.addAll(urlsToCache);
})
);
});
this.addEventListener("activate", event => {
// Clear `CACHE_NAME` that is not in the whitelist
const cacheWhitelist = ["service-worker-demo"];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
// Check the cache
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
);
});
this.addEventListener("fetch", event => {
const url = new URL(event.request.url);
if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
event.respondWith(
caches.match(event.request).then(resp => {
if (resp) {
console.log("fetch ", event.request.url, " has cache, retrieving from cache");
return resp;
} else {
console.log("fetch ", event.request.url, " has no cache, fetching from network");
return fetch(event.request);
// // If necessary, we don't need to match the request in the event response, we can directly cache all requests made
// return fetch(event.request).then(response => {
// return caches.open(CACHE_NAME).then(cache => {
// cache.put(event.request, response.clone());
// return response;
// });
// });
}
})
);
}
});
// cache.js
console.log("cache.js loaded");
// avatar.png
// [byte]png
https://github.com/WindrunnerMax/EveryDay
https://github.com/mdn/sw-test/
https://zhuanlan.zhihu.com/p/25459319
https://zhuanlan.zhihu.com/p/115243059
https://zhuanlan.zhihu.com/p/161204142
https://github.com/youngwind/service-worker-demo
https://mp.weixin.qq.com/s/3Ep5pJULvP7WHJvVJNDV-g
https://developer.mozilla.org/zh-CN/docs/Web/API/Cache
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
https://www.bookstack.cn/read/webapi-tutorial/docs-service-worker.md