Service Worker 实践

研究了一天,终于勉强能用了,但是目前还不支持使用 Pjax 的网站,例如本站,无法缓存 Pjax 加载的内容,目前还没有找到解决方法。

一开始使用的 Workbox 版本,但是为了一个简单的功能加载大量的资源有些不划算,所以改成了原生的版本,实现了以下功能

  • 采用 Network first 策略,网络优先,其次缓存
  • 支持离线页面,参考 预览
  • 跳过视频资源缓存
  • 支持CDN资源,永久缓存

manifest.json 可以在 https://www.simicart.com/manifest-generator.html/ 生成


index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!-- head -->
<link rel="manifest" href="/manifest.json">
<!-- footer -->
<script>
    if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
            navigator.serviceWorker.register('/sw.js');
        });
    }
</script>

sw.js workbox 版本(不推荐)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
const workboxVersion = '6.3.0';

importScripts(`https://storage.googleapis.com/workbox-cdn/releases/${workboxVersion}/workbox-sw.js`);
workbox.setConfig({
    debug: false
});

const CACHE_NAME = 'offline-html';
const FALLBACK_HTML_URL = '/offline.html';
self.addEventListener('install', async (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME).then((cache) => cache.add(new Request(FALLBACK_HTML_URL, {
            cache: "reload"
        })))
    );
});
workbox.navigationPreload.enable();
workbox.routing.registerRoute(
    new workbox.routing.NavigationRoute(async (params) => {
        try {
            return await (new workbox.strategies.NetworkFirst()).handle(params);
        } catch (error) {
            return caches.match(FALLBACK_HTML_URL, {
                cacheName: CACHE_NAME,
            });
        }
    })
);

workbox.core.setCacheNameDetails({
    prefix: "hazymoon",
    suffix: "v1",
    precache:'precache',
    runtime:'runtime'
});

workbox.core.skipWaiting();

workbox.core.clientsClaim();

workbox.precaching.precacheAndRoute([]);

workbox.precaching.cleanupOutdatedCaches();

workbox.routing.registerRoute(
    ({
        request
    }) => request.mode === 'navigate',
    new workbox.strategies.NetworkFirst({
        cacheName: 'pages',
        plugins: [
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [200],
            }),
        ],
    }),
);

// Images
workbox.routing.registerRoute(
    /\.(?:png|jpg|jpeg|gif|bmp|webp|svg|ico)$/,
    new workbox.strategies.CacheFirst({
        cacheName: "images",
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 1000,
                maxAgeSeconds: 60 * 60 * 24 * 30
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);

// Fonts
workbox.routing.registerRoute(
    /\.(?:eot|ttf|woff|woff2)$/,
    new workbox.strategies.CacheFirst({
        cacheName: "fonts",
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 1000,
                maxAgeSeconds: 60 * 60 * 24 * 30
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);

// Static Libraries
workbox.routing.registerRoute(
    /^https:\/\/cdn\.jsdelivr\.net/,
    new workbox.strategies.CacheFirst({
        cacheName: "static-libs",
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 1000,
                maxAgeSeconds: 60 * 60 * 24 * 30
            }),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);

workbox.routing.registerRoute(
    // Check to see if the request's destination is style for stylesheets, script for JavaScript, or worker for web worker
    ({
        request
    }) =>
    request.destination === 'style' ||
    request.destination === 'script' ||
    request.destination === 'worker',
    // Use a Stale While Revalidate caching strategy
    new workbox.strategies.StaleWhileRevalidate({
        // Put all cached files in a cache named 'assets'
        cacheName: 'assets',
        plugins: [
            // Ensure that only requests that result in a 200 status are cached
            new workbox.cacheableResponse.CacheableResponse({
                statuses: [200],
            }),
        ],
    }),
);

workbox.routing.registerRoute(
    // Check to see if the request's destination is style for an image
    ({
        request
    }) => request.destination === 'image',
    // Use a Cache First caching strategy
    new workbox.strategies.CacheFirst({
        // Put all cached files in a cache named 'images'
        cacheName: 'images',
        plugins: [
            // Don't cache more than 50 items, and expire them after 30 days
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 50,
                maxAgeSeconds: 60 * 60 * 24 * 30, // 30 Days
            }),
            // Ensure that only requests that result in a 200 status are cached
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [200],
            }),
        ],
    }),
);

workbox.googleAnalytics.initialize();

sw.js 原生版本(推荐)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const CACHE_NAME = 'hazymoon_sw';
const OFFLINE_URL = '/offline.html';
const OFFLINE_IMAGE = '/offline.jpg';

// 注册离线页面资源
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME).then((cache) => {
            cache.add(new Request(OFFLINE_URL, {
                cache: "reload"
            }))
            cache.add(new Request(OFFLINE_IMAGE, {
                cache: "reload"
            }))
        })
    );
});

// 如果支持,启用预加载
self.addEventListener('activate', (event) => {
    event.waitUntil((async () => {
        if ('navigationPreload' in self.registration) {
            await self.registration.navigationPreload.enable();
        }
    })());
    self.clients.claim();
});

self.skipWaiting();

self.addEventListener('fetch', (event) => {
    if (event.request.url.startsWith(self.location.origin) || event.request.url.match(/^https:\/\/cdn\.jsdelivr\.net/)) {
        event.respondWith((async () => {
            const cache = await caches.open(CACHE_NAME);
            try {
                const networkResponse = await fetch(event.request);
                if (networkResponse.status === 200) {
                    // 必须返回200才存储资源,防止出现错误,视频类的资源会返回206(成功但又没有完全成功)
                    await cache.put(event.request, networkResponse.clone());
                }
                return networkResponse;
            } catch (error) {
                // 如果网络请求失败,返回离线页面
                let cachedResponse = await cache.match(event.request);
                if (cachedResponse) {
                    return cachedResponse;
                } else {
                    return await cache.match(OFFLINE_URL);
                }
            }
        })());
    }
});

offline.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sorry!</title>

    <style>
        body {
            padding: 20px;
            background-color: #f4f8fb;
        }
        img {
            max-width: 100%;
            max-height: 50vh;
            display: block;
            margin: 30px 0;
        }
        h4, h3, h1, a {
            color: rgb(55, 65, 81);
        }
        a {
            display: block;
            font-size: 20px;
        }
    </style>
</head>
<body>
    <h1>呕吼,完蛋!</h1>
    <h3>你的网络好像出现了点问题,但是没关系,你仍然可以访问你之前访问过的页面</h3>
    <h4>现在请尽情欣赏下面这只可爱的兔狲吧</h4>

    <img src="/offline.jpg" alt="">

    <a href="/">点我返回首页</a>
</body>
</html>

现在页面在离线状态下访问未缓存的页面时将会显示