专栏名称: 前端大全
分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯
目录
相关文章推荐
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  13 小时前  
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  13 小时前  
前端早读课  ·  【第3451期】前端 TypeError ... ·  昨天  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
51好读  ›  专栏  ›  前端大全

ServiceWorker 让你的网页拥抱服务端的能力

前端大全  · 公众号  · 前端  · 2024-09-02 11:50

正文


作者:田八

https://juejin.cn/post/7165893682132959245

ServiceWorker 是一个运行在浏览器背后的独立线程,它拥有访问网络的能力,可以用来实现缓存、消息推送、后台自动更新等功能,甚至可以用来实现一个完整的 Web 服务器。

因为 ServiceWorker 运行在浏览器背后,因为这个特性,它可以实现一些不需要服务器参与的功能,比如消息推送、后台自动更新等。

什么是 ServiceWorker

ServiceWorker 提供了一个一对一的代理服务器,它可以拦截浏览器的请求,然后根据自己的逻辑来处理这些请求,比如可以直接返回缓存的资源,或者从网络上获取资源,然后将资源缓存起来,再返回给浏览器。

既然作为一个服务器,那么它就拥有着对应的生命周期,它没有传统的服务器那么复杂,它只有两个生命周期,分别是安装和激活,这个状态可以通过 ServiceWorker.state 来获取。

相信大家都不喜欢干巴巴的文字,下面我们来看一下 ServiceWorker 是怎么使用的,然后看一下它的生命周期,慢慢介绍它的功能。

ServiceWorker 的使用

注册 ServiceWorker

ServiceWorker 的注册是通过 navigator.serviceWorker.register 来完成的;

它接受两个参数:

  • 第一个参数是 ServiceWorker 的脚本地址

  • 第二个参数是一个配置对象,目前只有一个属性 scope ,用来指定 ServiceWorker 的作用域,它的默认值是 ServiceWorker 脚本所在目录。

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js', {
        scope'/'
    }).then(function (registration) {
        // 注册成功
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function (err) {
        // 注册失败 :(
        console.log('ServiceWorker registration failed: ', err);
    });
}

上面的代码我们分四个部分讲解:

  • 第一部分是判断浏览器是否支持 ServiceWorker ,如果不支持,那么就提示或者做其他的处理。

  • 第二部分是注册 ServiceWorker ,调用 navigator.serviceWorker.register 方法,它会返回一个 Promise 对象。

  • 第三部分是 register 方法的第一个参数,它是 ServiceWorker 的脚本地址,这个地址是相对于当前页面的地址的。

  • 第四部分是 register 方法的第二个参数,它是一个配置对象,目前只有一个属性 scope ,用来指定 ServiceWorker 的作用域,它的默认值是 ServiceWorker 脚本所在目录。

这里需要注意的就是第三部分和第四部分,我们先来看一下 register 的函数签名,再来讲注意的地方。

/**
 * 注册 ServiceWorker
 * @param {string} scriptURL ServiceWorker 脚本地址
 * @param {Object} options 配置项
 * @param {string} options.scope ServiceWorker 作用域
 * @returns {Promise}
 */

register(scriptURL, options)

函数签名看着很简单,但是我们需要注意的是 scriptURL scope 的值,它们的值是相对于当前页面的地址的,而不是相对于 ServiceWorker 脚本的地址的。

scriptURL 其实也没什么好说的,同之前讲的 Worker 一样,就是我们的脚本地址;

scope 的值是用来指定 ServiceWorker 的作用域的,它的默认值是 ServiceWorker 脚本所在目录,也就是 scriptURL 的值,但是我们可以通过 scope 来指定它的作用域,它的作用域是一个目录,它的值是相对于当前页面的地址的,也就是说,它的值是相对于 scriptURL 的值的。

上面说的有点绕,我们直接上代码,上面已经有了注册的代码了,我们现在补充 service-worker.js 的代码,看一下 scope 的值是怎么指定的。

// service-worker.js
self.addEventListener('install'function (event) {
    console.log('install');
});

self.addEventListener('activate'function (event) {
    console.log('activate');
});

self.addEventListener('fetch'function (event) {
    console.log('fetch');
});

上面的代码我都写好了之后,我们将它们放到服务器上,然后访问你托管的地址,打开控制台,你会看到如下的输出:

可以看到上面有三个输出,首先我们看到的是 ServiceWorker 的生命周期,经过了安装和激活,然后看到了注册成功的提示;

将页面刷新再看看控制台:

可以看到并没有进行安装和激活,这是因为我们的 ServiceWorker 已经注册成功了,它会一直存在,除非我们手动的注销它,否则它不会再次进行安装和激活。

注意:我这里出现了 4 次 fetch ,这是因为我有插件的原因,插件请求了一些资源,所以会触发 fetch 事件, fetch 事件会在后面讲到。

ServiceWorker 生命周期

上面我们已经成功的注册了 ServiceWorker ,那么它的生命周期我们肯定是需要关注一下的,它的生命周期有三个阶段,分别是安装、激活和运行。

安装

安装阶段是在 ServiceWorker 注册成功之后,浏览器开始下载 ServiceWorker 脚本的阶段;

这个阶段是一个异步的过程,我们可以在 install 事件中监听它,它的回调函数会接收到一个 event 对象;

我们可以通过 event.waitUntil 来监听它的完成状态,当它完成之后,我们需要调用 event.waitUntil 的参数,这个参数是一个 Promise 对象,当这个 Promise 对象完成之后,浏览器才会进入下一个阶段。

self.addEventListener('install'function (event) {
    console.log('install');
    event.waitUntil(
        // 这里可以做一些缓存的操作
    );
});

注意: event.waitUntil 不要乱用,它会阻塞浏览器的安装,如果你的 Promise 对象一直没有完成,那么浏览器就会一直处于安装的状态,这样会影响到浏览器的正常使用。

激活

激活阶段是在安装完成之后,浏览器开始激活 ServiceWorker 的阶段;

这个阶段也是一个异步的过程,我们可以在 activate 事件中监听它,它的回调函数会接收到一个 event 对象;

self.addEventListener('activate'




    
function (event) {
    console.log('activate');
    event.waitUntil(
        // 这里可以做一些清理缓存的操作
    );
});

不同于安装阶段,激活阶段不需要等待 event.waitUntil 的传递的 Permise 对象完成,它会立即进入下一个阶段。

但是永远不要传递一个可能一直处于 pending 状态的 Promise 对象,否则会导致 ServiceWorker 一直处在某一个状态而无法响应,导致浏览器卡死。

运行

运行阶段是在激活完成之后, ServiceWorker 开始运行的阶段;

这个阶段是一个长期存在的过程,我们可以在 fetch 事件中监听它,它的回调函数会接收到一个 event 对象;

self.addEventListener('fetch'function (event) {
    console.log('fetch');
});

任何请求拦截都是在这个阶段进行的,我们可以在这个阶段中对请求进行拦截,然后返回我们自己的响应。

ServiceWorker 请求拦截

上面我们已经成功的注册了 ServiceWorker ,并且它已经进入了运行阶段,那么我们就可以在这个阶段中对请求进行拦截了。

在上面我贴的图可以看到, ServiceWorker 连插件的请求都拦截了,这是因为 ServiceWorker 的优先级是最高的,它会拦截所有的请求,包括插件的请求。

我的插件请求了是一些 css 文件,也就是说 ServiceWorker 拦截了这些请求,然后返回了自己的响应,这个响应就是我们在 ServiceWorker 中缓存的资源。

插件的请求咱们不用管,现在来看看我们的 ServiceWorker 到底能拦截多少种类型的请求:

html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <link rel="stylesheet" href="index.css">
head>
<body>

<script src="axios.js">script>
<script>
    // 注册service worker
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('/service-worker.js', {
            scope'/'
        }).then(function (registration) {
            // 注册成功
            console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function (err) {
            // 注册失败 :(
            console.log('ServiceWorker registration failed: ', err);
        });
    }

    // 使用axios发送请求
    axios.get('/').then(function (response) {
        console.log('axios 成功');
    });

    // 使用XMLHttpRequest发送请求
    const xhr = new XMLHttpRequest();
    xhr.open('GET''/');
    xhr.send();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            console.log('XMLHttpRequest 成功');
        }
    }

    // 使用fetch发送请求
    fetch('/').then(function (response) {
        console.log('fetch 成功');
    });
script>

body>
html>

上面的代码中我发送了五个请求,分别是请求 axios.js axios 发送请求, XMLHttpRequest 发送请求, fetch 发送请求,最头部还有一个 css 请求;

css 的内容自己随意发挥,我这里就不贴了。

可以看到, ServiceWorker 只进入了7次 fetch 事件,也就是说只拦截了7次请求,我们可以通过 event.request.url 来查看请求的地址。

self.addEventListener('fetch'function (event) {
    console.log('fetch', event.request.url);
});

通过打印请求地址,发现 axios.js 没有进入 fetch 事件,但是并不影响我们的结果。

关于静态资源为什么没有进入 fetch 事件,我这里没有查到相关资料,但是其实确实是进入了 fetch 事件。

ServiceWorker 监听事件

上面因为我们只监听了 fetch 事件,所以只有 fetch 请求被拦截了,那么我们可以监听哪些事件呢?

从最开始的生命周期的两个事件, install activate ,到后面的 fetch 网络请求的,还有其他什么事件呢?

现在就来看看 ServiceWorker 的事件列表:

  • install :安装事件,当 ServiceWorker 安装成功后,就会触发这个事件,这个事件只会触发一次。

  • activate :激活事件,当 ServiceWorker 激活成功后,就会触发这个事件,这个事件只会触发一次。

  • fetch :网络请求事件,当页面发起网络请求时,就会触发这个事件。

  • push :推送事件,当页面发起推送请求时,就会触发这个事件。

  • sync :同步事件,当页面发起同步请求时,就会触发这个事件。

  • message :消息事件,当页面发起消息请求时,就会触发这个事件。

  • messageerror :消息错误事件,当页面发起消息错误请求时,就会触发这个事件。

  • error :错误事件,当页面发起错误请求时,就会触发这个事件。

可以看到最后三个是我们的老伙伴了, message messageerror error ,它在这个基础上还增加了两个事件, push sync

翻了很多资料, ServiceWorker 还可以监听 notification 事件,但是目前我还没有找到相关的资料,后续找到了我会单独写一篇文章来讲解。

message messageerror error 这三个事件,我们在上一篇文章中已经讲解过了,就是主线程和 Worker 之间的通信,文末有链接,可以去看看。

push sync 这两个事件,今天这里不详解,后续我会单独写一篇文章来讲解。

ServiceWorker 缓存

缓存是我们日常开发中经常会用到的一个功能, ServiceWorker 也提供了缓存的功能,我们可以通过 ServiceWorker 来缓存我们的静态资源,这样就可以离线访问我们的页面了。

ServiceWorker 的缓存是基于 CacheStorage 的,它是一个 Promise 对象,我们可以通过 caches 来获取它;

caches.open('my-cache').then(function (cache) {
    // 这里可以做一些缓存的操作
});

CacheStorage 提供了一些方法,我们可以通过这些方法来对缓存进行操作;

添加缓存

我们可以通过 cache.put 来添加缓存,它接收两个参数,第一个参数是 Request 对象,第二个参数是 Response 对象;

caches.open('my-cache').then(function (cache) {
    cache.put(new Request('/'), new Response('Hello World'));
});

获取缓存

我们可以通过 cache.match 来获取缓存,它接收一个参数,这个参数可以是 Request 对象,也可以是 URL 字符串;

caches.open('my-cache').then(function (cache) {
    cache.match('/').then(function (response) {
        console.log(response);
    });
});

删除缓存

我们可以通过 cache.delete 来删除缓存,它接收一个参数,这个参数可以是 Request 对象,也可以是 URL 字符串;

caches.open('my-cache').then(function (cache) {
    cache.delete('/').then(function () {
        console.log('删除成功');
    });
});

清空缓存

我们可以通过 cache.keys 来获取缓存的 key ,然后通过 cache.delete 来删除缓存;

caches.open('my-cache').then(function (cache) {
    cache.keys().then(function (keys) {
        keys.forEach(function (key) {
            cache.delete(key);
        });
    });
});

ServiceWorker 缓存策略

ServiceWorker 的缓存策略是基于 fetch 事件的,我们可以在 fetch 事件中监听请求,然后对请求进行拦截,然后返回我们自己的响应;

self.addEventListener('fetch'function (event) {
    event.respondWith(
        caches.match(event.request).then(function (response) {
            if (response) {
                return response;
            }
            return fetch(event.request);
        })
    );
});

上面的代码是一个最简单的缓存策略,它会先从缓存中获取请求,如果缓存中没有请求,那么就会从网络中获取请求;

缓存资源

文章开始我们介绍了 ServiceWorker 的生命周期,然后又详解了 fetch 事件,最后又讲了一堆缓存的东西,这些都是为我们接下来的内容做铺垫,接下来我们缓存一些静态资源,然后离线访问我们的页面;

还是上面 fetch 事件的例子,我们请求了 6 个资源,其中 1 是 index.html ,1 是 axios.js ,1 个是 index.css ,剩余的 3 个都是请求的'/',也是我们的 index.html

但是上面的例子中我们什么都没做,所以我们的页面是没有缓存的,我们可以通过 cache.addAll 来缓存一些资源;

通常我们会在 install 事件中缓存一些资源,因为 install 事件只会触发一次,并且会阻塞 activate 事件,所以我们可以在 install 事件中缓存一些资源,然后在 activate 事件中删除一些旧的资源;

self.addEventListener('install'function (event) {
    event.waitUntil(
        caches.open('my-cache').then(function (cache) {
            return cache.addAll([
                '/',
                '/index.css',
                '/axios.js',
                '/index.html'
            ]);
        })
    );
});

上面的代码中我们缓存了刚才提到的所有资源,缓存了之后当然是使用缓存的资源了,所以我们可以在 fetch 事件中返回缓存的资源;

注意:上面缓存的所有资源一定都是确定的存在的,不能出现除状态码为 200 以外的其他状态码,否则缓存会失败;

self.addEventListener('fetch'function (event) {
    event.respondWith(
        caches.match(event.request).then(function (response) {
            if (response) {
                return response;
            }
            return fetch(event.request);
        })
    );
});

上面的代码中我们使用 caches.match 来匹配请求,如果匹配到了,那么就返回缓存的资源,如果没有匹配到,那么就从网络中获取资源,这也就是我们刚才提到的缓存策略: 缓存优先

看看上面的图,当我们第一次访问页面的时候,我们的页面是没有缓存的,所以我们的页面是从网络中获取的,当我们刷新页面的时候,我们的页面是从缓存中获取的,可以看到来源是 ServiceWorker







请到「今天看啥」查看全文