preface
Before you follow this tutorial, you need to have at least a basic understanding of NodeJS, NPM, and HTTP caching, as service workers involve caching.
More basic knowledge about service workers can be found here. This paper focuses on the implementation of offline caching.
If you have some knowledge about service workers, you can directly see the implementation.
Knowledge point summary
- Introduction of the service worker
- Implement a simple static resource server based on NodeJS
- Basic offline cache implementation
- Offline cache updates (emphasis)
- A combination of offline caching and online services
Introduction of the service worker
Service worker translated into Chinese is called service worker thread, which is an event-driven worker registered under the specified source and path. The pages or websites controlled by service worker can intercept and modify access and resource requests, and cache resources in fine granularity. Therefore, the service worker can also realize offline caching when the network is not available.
The service worker is independent of the worker context outside of the main thread, so it cannot access the DOM, so it does not block. It is designed to be completely asynchronous, and synchronization apis such as XHR and localStorage cannot be used in the service worker.
Service workers can only be hosted by HTTPS, since the ability to modify network requests is too dangerous to expose to man-in-the-middle attacks. In the user privacy mode of Firefox, the service worker is unavailable. In addition, the local environment localhost can also use the service worker.
Project directory structure
├ ─ ─ the README. Md ├ ─ ─ package. The json ├ ─ ─ public │ └ ─ ─ the favicon. Ico ├ ─ ─ for server js ├ ─ ─ the SRC │ ├ ─ ─ index. The CSS │ ├ ─ ─ index. The HTML │ └ ─ ─ ├── ├─ ├─ ├─ ├.htmCopy the code
Create a new folder and name it service-worker. To locate the directory, run NPM init and press Enter. Install serve-Favicon, Nodemon, Express:
NPM install serve-favicon Express nodemon --save-dev or yarn add serve-favicon Express nodemon --devCopy the code
Then create sw SRC public under the project root and create the corresponding files.
Implement a simple static resource server based on NodeJS
In a real project, we should use nginx and others as static resource servers. In this example, we simply use NodeJS.
server.js
const path = require("path");
const express = require("express");
const favicon = require('serve-favicon');
const app = express();
app.get("*", (req, res, next) => {
console.log(req.url);
next();
})
app.use(favicon(path.join(__dirname, "public"."favicon.ico")));
const options = {
setHeaders (res, filePath, stat) {
res.set({
"cache-control": "no-cache"
})
}
}
app.use(express.static("src", options));
app.use(express.static("sw", {
maxAge: 0
}))
app.listen("9000", () = > {console.log("server start at localhost:9000");
})
Copy the code
In the server.js file, the static resource cache type in the SRC directory is set to negotiation cache. Each time the client obtains resources, it verifies the validity of the file to the server to confirm whether to use local cache. Server-worker. js under sw is set to permanent cache 0, that is, no cache. The client will obtain complete resources from the server every time, so server-worker.js must not be cached
Add startup commands in package.json
"start": "nodemon server.js"
Copy the code
And then start the server
Basic offline cache implementation
Start by writing some random content in index.html and index.css.
src/index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>service-worker</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div class="box">
<div>hello service worker v1 </div>
</div>
<script src="./index.js"></script>
</body>
</html>
Copy the code
src/index.css
.box {
text-align: center;
color: red;
}
Copy the code
Then, the registration logic of service worker is implemented in index.js, and the support degree of service worker by mainstream browsers can be checked in Caniuse.
src/index.js
function serviceRegister () {
const { serviceWorker } = navigator;
if(! serviceWorker) {console.warn("your browser not support serviceWorker");
return;
}
window.addEventListener("load".async() = > {let sw = null;
const regisration = await serviceWorker.register("./service-worker.js");
sw = regisration.installing || regisration.waiting || regisration.active;
sw && sw.addEventListener("statechange", (e) => {
const { state } = e.target;
console.log(state);
});
});
}
serviceRegister();
Copy the code
The serviceWorker is first retrieved from the Navigator and then tested for support. When the browser resource is loaded, call serviceworker.register (url, {scope: “XXXX “}) register server worker, url is js file path of service workder content, scope is control domain of Service worker, default value is based on current location, that is /, can be customized.
The serviceworker. register function returns a regisration. The worker is registered asynchronously. Therefore, you can only use installing, waiting, and active in Regisration to obtain the registered instance, and then add the Statechange event to the instance to listen for the status change.
How do I view my registered SW when I register successfully? Open Chrome ://inspect/#service-workers to see what SW is being registered in the current browser. There is also a Chrome ://serviceworker-internals, which is used to view all registered sw files in the current browser. In addition, open the Chrome console under the current page, switch to the Application option, there is a Service Workers secondary option below, you can view the current SW state, the application Cache below can view the current Cache version number.
The browser opens the console, then localhost:9000, and you can see the state of the sw cycle in the log output
installed
activating
activated
Copy the code
It can be seen that when registering sw for the first time, three cycles of INSTALLED, activating and activated will be completed, and the current sw state can be seen as the following figure when switching to Application/Service Workers on the console:
Now there is nothing in service-worker.js, so we add some content to it:
service-worker.js
const _this = this;
const version = "v1";
const cacheFile = [
"/"."/index.html"."index.css"."index.js"
]
this.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(version)
.then((cache) = > {
return cache.addAll(cacheFile)
})
)
})
this.addEventListener("fetch".async (event) => {
const { request } = event;
event.respondWith(
caches
.match(request.clone())
.catch((a)= > {
return fetch(request.clone()).catch()
})
);
});
this.addEventListener("activate", (event) => {
const wihleList = [version];
event.waitUntil(
caches
.keys()
.then((keyList) = > {
return Promise.all(
keyList.map((key) = > {
if(! wihleList.includes(key)) {returncaches.delete(key); }}))}))});Copy the code
In the server-worker thread, add install, fetch, activate events to the current worker. Add the cache version number and the specific cache file in the install event rollback function. The FETCH event intercepts the client request, we first read the content from the cache, and continue to request the server if it is not found in the cache. Activate clears caches that are not the current cache version.
After the modification of service-worker is completed, refresh the browser and switch to the console to see the current SW state as shown in the figure below:
The old SW is still in service, the control of the page is still in the old SW, the new SW has been installed, but it is inactive, so it does not have the control of the page. The sw update will be discussed later, we manually click skipWaiting for the new SW to be activated immediately and then take control of the page.
Note: In the process of development and debugging, we often need to do skipWaiting, unRegister, delete cache and other operations on the console for sw, but you should not expect users to do this, so when launching online version, you must be careful, cache is wrong, the user loaded content error. But a serious breakdown…
After manually clicking skipWaiting, see that the new SW has taken effect. Refresh the browser and in the Network option you can see that the request for cached content has been intercepted and retrieved from the SW cache. In the Chrome console, change the network state to Offline and refresh the browser again. Well, the Internet is down, but you can still read from the local cache.
At this point, the basic principle of service-worker offline caching is clear and the basic functions are implemented, but if you think that this can be used in production environment, it is definitely a big deal, because the use of sw app updates can be quite magical.
Updates to the offline cache
In service-worker applications, updates are divided into SW updates and page resource updates.
Update service – worker
Following the example above, every time service-worker.js is updated, the client reinstalls the new sw and waits for the old SW-controlled pages to close or for the entire browser to close before the new SW is activated. In the SW process, the skipWaiting method is provided, which can skip the new SW waiting state and directly activate to obtain the control of the page under the scope.
src/service-worker.js
. const version ="v2"; . this.addEventListener("install", (event) => {
this.skipWaiting(); . })Copy the code
Add skipWaiting(), change the cache version to v2, change the browser console network to online, and refresh the browser again. Check the application of the console, the SW has no skipWaiting state, and the new SW is activated immediately after the installation is completed. There are also a number of problems with skipWaiting:
- The browser actively updates the SW in the background, and the SW caches new files. The next time the user accesses the URL, the sw directly retrieves the new cache. If the sw update is triggered by the user accessing the URL, since the SW registration and installation update are asynchronous by a separate thread, then when our URL is opened, it is still controlled by the old SW, so we get the old resource.
Modify the SRC/index. HTML
<div>hello service worker v3 </div>
Copy the code
Modify the sw/service – worker. Js
const version = "v3";
Copy the code
Refresh the browser, the new sw has been activated, but the page is still Hello Service worker v1, refresh again to hello Service worker v3.
Solution: When registering a service worker, add a ControllerChange event to refresh the page or notify the user to refresh the browser in the form of a message when listening to a new SW and obtaining page control
Obviously, this approach works, but it was rejected because it is too violent to actively refresh the page, and the result of notifying the user to refresh the browser is not controllable.
-
The old SW may be different from the new sw, and the replacement may cause many unknown errors that are uncontrollable and difficult to reproduce.
-
The sw is not updated, but the resource is updated, and the user still accesses the URL through the old SW cache.
Modify the SRC/index. HTML
<div>hello service worker v4 </div>
Copy the code
Refresh the browser and the page content is still Hello Service worker V3.
A combination of offline caching and online services
In view of various problems caused by offline cache update, I have been thinking about it for a long time. Meanwhile, I have also seen the application of service worker in other famous enterprises’ products. Finally, I have come up with a solution: Online resources are given priority in the pages controlled by SW, and SW acts as the proxy server of local and online servers. When there is an online resource acquisition error (server down, network unavailable, etc.), the SW local cache is used.
The HTML file is obtained from the online file first. After obtaining the HTML file, set the cache so that the next time the HTML file is obtained from the local cache when the error occurs, the last file is obtained. In short, when we can get the server resources, we need to ensure that the HTML is always up to date, because the VARIOUS JS and CSS resources in THE HTML will add the version number, so these JS and CSS resource requests will be the correct version number, in addition to the HTML file resources, we prefered to obtain from the local. If a request is not cached locally, it is fetched online and cached for later use. (In a real project, the request URI should be compared to its own cacheFile, which is its own list of cached files. The local cache storage takes priority over all non-HTML requests.)
sw/service-worker.js
function setCache (req, res) {
caches
.open(version)
.then((cache) = >{ cache.put(req, res); })}this.addEventListener("fetch".async (event) => {
const { request } = event;
if (request.headers.get("Accept").indexOf("text/html")! = =- 1) {
event.respondWith(
fetch(request.clone())
.then((response) = > {
if (response) {
setCache(request.clone(), response.clone())
return response.clone();
}
return caches.match(request.clone());
}).catch((e) = > {
returncaches.match(request.clone()); }))return;
}
event.respondWith(
caches
.match(request.clone())
.then((response) = > {
if (response) {
return response;
}
return (
fetch(request.clone())
.then((fetchResponse) = > {
// For non-HTML resources, the actual project should not cache all requests because the local cache storage is limited
// The requested resource should be compared to the cacheFile, and cached if it matches, or not,
// Fetch only acts as a service request relay
setCache(request.clone(), fetchResponse.clone());
returnfetchResponse.clone(); })); }).catch((e) = > {
console.log(e); })); });Copy the code
After modifying service-worker.js, we unregister sw in console, clean up cache in cache storage, and change HTML content to Hello service worker v1. Then change verson in service-worker.js to v1 and refresh the page, which is equivalent to the user accessing the URL for sw registration for the first time.
After that, you can perform various operations, such as HTML content modification, sw cache version modification or other sw content modification, or disconnection operation, or their own cache storage request, etc., refresh the browser after each operation, see the effect and think about the result and why!
Note:
In this example, there is no version number added to index.js index.css. If you want to modify the content inside, you need to add a version number, otherwise you get old resources in the cache.
In the process of development and debugging, we often need to perform skipWaiting, unRegister, delete cache and other operations on the console for SW, observe and think about the state and change of SW in multiple scenarios, and design a reasonable cache scheme according to our own business.
The basic offline cache scheme has been implemented, but it is only at the demo stage. In practical application, for example, we have three-party CDN, and all resources have version numbers after our project is packaged by tools such as Webpack. It is unrealistic to manually modify service-worker.js every time, and it is very error-prone. There is more that needs to be done to apply serviceWorker to production environments with tools such as WebPack.
The next article will cover serviceWorker’s ability to automatically generate offline caching applications in conjunction with WebPack.
The github address of this article, if there is any help, welcome star, if you can put forward valuable suggestions or issues, that would be even better.