专栏首页YuanXinService Worker离线缓存实战

Service Worker离线缓存实战

背景介绍

最近实战了 Service Worker(以下简称“sw”)来进行网站缓存,以实现离线状态下,网站仍然可以正常使用。

尤其对于个人博客这种以内容为主体的静态网站,离线访问和缓存优化尤其重要;并且 Ajax 交互较少,离线访问和缓存优化的实现壁垒因此较低。

专注前端与算法的系列干货分享,转载请声明出处:原文链接: xxoo521.com

环境准备

虽然 sw 要求必须在 https 环境下才可以使用,但是为了方便开发者,通过localhost或者127.0.0.1也可以正常加载和使用。

利用 cnpm 下载http-servernpm install http-server -g

进入存放示例代码的文件目录,启动静态服务器:http-server -p 80

最后,准备下 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>Documenttitle>
    head>
    <body>
        <img src="./image.png" height="300" width="300" />
        <img
            src="https://user-gold-cdn.xitu.io/2017/10/4/50e8f96bbcb3bc644a083a409ce0ce2d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1"
        />
        <h3>一些提示信息sdfsfh3>
        <ul>
            <li>浏览器是否支持:<span id="isSupport">span>li>
            <li>service worker是否注册成功:<span id="isSuccess">span>li>
            <li>当前注册状态:<span id="state">span>li>
            <li>当前service worker状态:<span id="swState">span>li>
        ul>
        <script src="/script.js">script>
    body>
html>

注册 Service Worker

我们通过script.js来判断浏览器是否支持 serviceWorker,并且加载对应的代码。script.js内容如下:

window.addEventListener("load", event => {
    // 判断浏览器是否支持
    if ("serviceWorker" in navigator) {
        console.log("支持");
        window.navigator.serviceWorker
            .register("/sw.js", {
                scope: "/"
            })
            .then(registration => {
                console.log("注册成功");
            })
            .catch(error => {
                console.log("注册失败", error.message);
            });
    } else {
        console.log("不支持");
    }
});

注册时机

如上所示,最好在页面资源加载完成的事件(window.onload)之后注册 serviceWorker 线程。因为 serviceWorker 也会浪费资源和网络 IO,不能因为它而影响正常情况下(网络信号 ok 的情况)的使用体验。

拦截作用域

之后,我们需要用 serviceWorker 线程来拦截资源请求,但不是所有的资源都能被拦截,这主要是看 serviceWorker 的作用域:它只管理其路由和子路由下的资源文件

例如上面代码中,/sw.js是 serviceWorker 脚本,它拦截根路径下的所有静态资源。如果是/static/sw.js,就只拦截/static/下的静态资源。

开发者也可以通过传递scope参数,来指定作用域。

Service Worker 最佳实践

笔者爬了很久的坑,中途看了很多人的博客,包括张鑫旭老师的文章。但是实践的时候都出现了问题,直到读到了百度团队的文章才豁然开朗。

为了让sw.js的逻辑更清晰,这里仅仅展示最后总结出来的最优代码。如果想了解更多,可以跳到本章最后一个部分《参考链接》。

sw 的生命周期

对于 sw,它的生命周期有 3 个部分组成:install -> waiting -> activate。开发者常监听的生命周期是 install 和 activate。

这里需要注意的是:两个事件的回调监听函数的参数上都有waitUntil函数。开发者传递到它的promise可以让浏览器了解什么时候此状态完成

如果难理解,可以看下面这段代码:

const VERSION = "v1";

self.addEventListener("install", event => {
    // ServiceWoker注册后,立即添加缓存文件,
    // 当缓存文件被添加完后,才从install -> waiting
    event.waitUntil(
        caches.open(VERSION).then(cache => {
            return cache.addAll(["./index.html", "./image.png"]);
        })
    );
});

更新 Service Worker 代码

对于缓存的更新,可以通过定义版本号的方式来标识,例如上方代码中的 VERSION 变量。但对于 ServiceWorker 本身的代码更新,需要别的机制。

简单来说,分为以下两步:

  1. 在 install 阶段,调用 self.skipWaiting() 跳过 waiting 阶段,直接进入 activate 阶段
  2. 在 activate 阶段,调用 self.clients.claim() 更新客户端 ServiceWorker

代码如下:

const VERSION = "v1";

// 添加缓存
self.addEventListener("install", event => {
    // 跳过 waiting 状态,然后会直接进入 activate 阶段
    event.waitUntil(self.skipWaiting());
});

// 缓存更新
self.addEventListener("activate", event => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all([
                // 更新所有客户端 Service Worker
                self.clients.claim(),

                // 清理旧版本
                cacheNames.map(cacheName => {
                    // 如果当前版本和缓存版本不一样
                    if (cacheName !== VERSION) {
                        return caches.delete(cacheName);
                    }
                })
            ]);
        })
    );
});

再探更新

上一部分说了更新 sw 的 2 个步骤,但是为什么这么做呢?

因为对于同一个 sw.js 文件,浏览器可以检测到它已经更新(假设旧代码是 sw1,新代码是 sw2)。由于 sw1 还在运行,以及默认只运行一个同名的 sw 代码,所以 sw2 处于 waiting 状态。所以需要强制跳过 waiting 状态

进入 activate 后,还需要取得“控制权”,并且弃用旧代码 sw1。上方的代码顺便清理了旧版本的缓存。

资源拦截

在代码的最后,需要监听 fetch 事件,并且进行拦截。如果命中,返回缓存;如果未命中,放通请求,并且将请求后的资源缓存下来。

代码如下:

self.addEventListener("fetch", event => {
    event.respondWith(
        caches.match(event.request).then(response => {
            // 如果 Service Workder 有自己的返回
            if (response) {
                return response;
            }

            let request = event.request.clone();
            return fetch(request).then(httpRes => {
                // http请求的返回已被抓到,可以处置了。

                // 请求失败了,直接返回失败的结果就好了。。
                if (!httpRes || httpRes.status !== 200) {
                    return httpRes;
                }

                // 请求成功的话,将请求缓存起来。
                let responseClone = httpRes.clone();
                caches.open(VERSION).then(cache => {
                    cache.put(event.request, responseClone);
                });

                return httpRes;
            });
        })
    );
});

效果测试

启动服务后,进入 localhost ,打开 devtools 面板。可以看到资源都通过 ServiceWorker 缓存加载进来了。

现在,我们打开离线模式,

离线模式下照样可以访问:

最后,我们修改一下 html 的代码,并且更新一下 sw.js 中标识缓存版本的变量 VERSION:

在第 2 次刷新后,通过上图可以看到,缓存版本内容已更新到 v2,并且左侧内容区已经被改变。

参考链接

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • LeetCode 79.单词搜索 - JavaScript

    单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

    心谭博客
  • 【剑指offer:圆圈中最后剩下的数字】JavaScript实现

    题目描述:0,1,,n-1 这 n 个数字排成一个圆圈,从数字 0 开始,每次从这个圆圈里删除第 m 个数字。求出这个圆圈里剩下的最后一个数字。例如,0、1、2...

    心谭博客
  • 十三:自动生成HTML文件

    为了实现这个功能,需要借助HtmlWebpackPlugin根据指定的index.html模板生成对应的 html 文件,还需要配合html-loader处理 ...

    心谭博客
  • 大数据新机遇,教育系统将建设完整安全体系

    随着网络规模的扩大,Web应用承载的业务系统越来越复杂,Web系统也受到越来越多的攻击和威胁。大数据时代,网络安全也直接影响到每一个用户的个人信息安全,但是大数...

    安恒信息
  • Python爬虫进阶必备 | 关于某汽车交易网加密 Cookie 的分析

    这个网站在爬取的时候需要先获取一个名为antipas Cookie,见名知意,接下来就一块看看这个字段怎么搞。

    咸鱼学Python
  • Redis 的 LBS 支持

    Redis 刚刚发布了 3.2 版本,其中官方支持了地理位置相关的 GEO API 非常适合简单的位置服务场景,例如 查找附近的商家、计算用户与商家的距离 使...

    dys
  • 数据库概论

    SuperHeroes
  • 《扫雷》的相变(CS AI)

    我们研究了经典的《扫雷》游戏中玩家在二维格上推断地雷位置的平均情况复杂度。玩扫雷游戏是Co-NP-Complete【著名的希尔伯特23个问题中目前仍未解决的问题...

    邱邱邱
  • 初探Grunt

    最近打算学习一些web编程的知识,今天学习了Grunt这个工具的用法,这里简要地对学习的知识点进行个总结。

    王云峰
  • java web部署 启动线程 (监听socket等)

    用户1258909

扫码关注云+社区

领取腾讯云代金券