【PWA学习】3. 让你的 WebApp 离线可用

news2024/11/29 9:50:00

引言

PWA 其中一个令人着迷的能力就是离线(offline)可用

即使在离线状态下,依然可以访问的 PWA

离线只是它的一种功能表现而已,具体说来,它可以:

  • 让我们的Web App在无网(offline)情况下可以访问,甚至使用部分功能,而不是展示“无网络连接”的错误页;
  • 让我们在弱网的情况下,能使用缓存快速访问我们的应用,提升体验;
  • 在正常的网络情况下,也可以通过各种自发控制的缓存方式来节省部分请求带宽;
  • ……

而这一切,其实都要归功于PWA背后的英雄 —— Service Worker

Service Worker

丰富的离线体验、定期的后台同步以及推送通知等通常需要将面向本机应用的功能将引入到网络应用中。service worker提供了所有这些功能所依赖的技术基础

什么是 service worker

service worker是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门。因此,它不会阻塞浏览器脚本的运行,同时也无法直接访问浏览器相关的API(例如:DOM、localStorage等)。此外,即使在离开你的 Web App,甚至是关闭浏览器后,它仍然可以运行。它就像是一个在 Web 应用背后默默工作的勤劳小蜜蜂,处理着缓存、推送、通知与同步等工作。所以,要学习PWA,绕不开的就是Service Worker

需要特别注意的是,由于 Service Worker 所具有的强大能力,因此规范规定,Service Worker 只能运行在 HTTPS 域下。然而我们开发时候没有 HTTPS 怎么办?别着急,还有一个贴心的地方——为方便本地开发,Service Worker 也可以运行在 localhost(127.0.0.1)域下

Service Worker是如何实现离线可用的

首先,当访问一个web网站时,我们实际上做了什么呢?总体上来说,我们通过与与服务器建立连接,获取资源,然后获取到的部分资源还会去请求新的资源(例如html中使用的css、js等)。所以我们访问一个网站,就是在获取/访问这些资源

可想而知,当处于离线或弱网环境时,我们无法有效访问这些资源,这就是制约我们的关键因素。因此,一个最直观的思路就是:如果我们把这些资源缓存起来,在某些情况下,将网络请求变为本地访问,这样是否能解决这一问题?是的。但这就需要我们有一个本地的 cache,可以灵活地将各类资源进行本地存取

如何获取所需的资源?

有了本地的 cache 还不够,我们还需要能够有效地使用缓存、更新缓存与清除缓存,进一步应用各种个性化的缓存策略。而这就需要我们有个能够控制缓存的“worker”——这也就是Service Worker的部分工作之一。顺便多说一句,可能有人还记得 ApplicationCache 这个API。当初它的设计同样也是为了实现Web资源的缓存,然而就是因为不够灵活等各种缺陷,如今已被 Service Worker 与 cache API 所取代了

Service Worker 有一个非常重要的特性:你可以在 Service Worker 中监听所有客户端(Web)发出的请求,然后通过 Service Worker 来代理,向后端服务发起请求。通过监听用户请求信息,Service Worker可以决定是否使用缓存来作为Web请求的返回

下图展示普通 Web App 与添加了 Service Worker 的 Web App 在网络请求上的差异:

普通 Web 请求(上)与使用 Service Worker 代理(下)的区别

注: 虽然图中好像将浏览器、SW(Service Worker)与后端服务三者并列放置了,但实际上浏览器(你的Web应用)和SW都是运行在你的本机上的,所以这个场景下的SW类似一个“客户端代理”

如何使用Service Worker实现离线可用的“秒开”应用

注册 Service Worker

我们的应用始终应该是渐进可用的,在不支持 Service Worker 的环境下,也需要保证其可用性。要实现这点,可以通过特性检测,在 index.js 中来注册我们的 Service Worker(sw.js)

  • public/index.js
// 注册 service worker,service worker 脚本文件为 sw.js
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js').then(function () {
        console.log('Service Worker 注册成功');
    });
}

Service Worker 的各类操作都被设计为异步,用以避免一些长时间的阻塞操作。这些API都是以 Promise 的形式来调用的

Service Worker 的生命周期

当我们注册了 Service Worker 后,它会经历生命周期的各个阶段,同时会触发相应的事件

Service Worker 生命周期

整个生命周期包括了:installing => installed => activating => activated => redundant。当 Service Worker 安装(installed)完毕后,会触发 install 事件;而激活(activated)后,则会触发activate事件

下面的例子监听了install事件:

// 监听install事件
self.addEventListener('install', function (e) {
    console.log('Service Worker 状态: install');
});

self 是 Service Worker 中一个特殊的全局变量,类似于我们最常见的 window 对象。self 指向当前这个 Service Worker

缓存静态资源

要使我们的 Web App 离线可用,就需要将所需资源缓存下来。我们需要一个资源列表,当 Service Worker 被激活时,会将该列表内的资源缓存进 cache

  • sw.js
// sw.js
var cacheName = 'bs-0-2-0';
var cacheFiles = [
    '/',
    './index.html',
    './index.js',
    './style.css',
    './img/book.png',
    './img/loading.svg'
];

// 监听install事件,安装完成后,进行文件缓存
self.addEventListener('install', function (e) {
    console.log('Service Worker 状态: install');
    var cacheOpenPromise = caches.open(cacheName).then(function (cache) {
        return cache.addAll(cacheFiles);
    });
    e.waitUntil(cacheOpenPromise);
});

可以看到,首先在cacheFiles中我们列出了所有的静态资源依赖。注意其中的'/',由于根路径也可以访问我们的应用,因此不要忘了将其也缓存下来。当Service Worker install时,我们就会通过caches.open()与cache.addAll()方法将资源缓存起来。这里我们给缓存起了一个cacheName,这个值会成为这些缓存的key。

上面这段代码中,caches是一个全局变量,通过它我们可以操作Cache相关接口

Cache 接口提供缓存的 Request / Response 对象对的存储机制。Cache 接口像 workers 一样, 是暴露在 window 作用域下的。尽管它被定义在 service worker 的标准中, 但是它不必一定要配合 service worker 使用。——MDN

使用缓存的静态资源

到目前为止,我们仅仅是注册了一个 Service Worker,并在其 install 时缓存了一些静态资源。但现在依然无法离线使用

因为我们仅仅缓存了这些资源,然而浏览器并不知道需要如何使用它们;换言之,浏览器仍然会通过向服务器发送请求来等待并使用这些资源

这就要用到前半部分介绍 Service Worker 时提到了“客户端代理”——用 Service Worker 来帮我们决定如何使用缓存

下图是一个简单的策略:

有 cache 时的静态资源请求流程
无 cache 时的静态资源请求流程
  1. 浏览器发起请求,请求各类静态资源(html/js/css/img);
  2. Service Worker 拦截浏览器请求,并查询当前 cache;
  3. 若存在 cache 则直接返回,结束;
  4. 若不存在 cache,则通过 fetch 方法向服务端发起请求,并返回请求结果给浏览器
  • sw.js
self.addEventListener('fetch', function (e) {
    // 如果有cache则直接返回,否则通过fetch请求
    e.respondWith(
        caches.match(e.request).then(function (cache) {
            return cache || fetch(e.request);
        }).catch(function (err) {
            console.log(err);
            return fetch(e.request);
        })
    );
});

fetch 事件会监听所有浏览器的请求。e.respondWith()方法接受 Promise 作为参数,通过它让 Service Worker 向浏览器返回数据。caches.match(e.request)则可以查看当前的请求是否有一份本地缓存:如果有缓存,则直接向浏览器返回 cache;否则 Service Worker 会向后端服务发起一个fetch(e.request)的请求,并将请求结果返回给浏览器

到目前为止,运行我们的 demo:当第一联网打开 Web App 后,所依赖的静态资源就会被缓存在本地;以后再访问时,就会使用这些缓存而不发起网络请求。因此,即使在无网情况下,我们似乎依旧能“访问”该应用

更新静态缓存资源

当我们将资源缓存后,除非注销(unregister)sw.js、手动清除缓存,否则新的静态资源将无法缓存

解决这个问题的一个简单方法就是修改cacheName。由于浏览器判断 sw.js 是否更新是通过字节方式,因此修改cacheName会重新触发 install 并缓存资源。此外,在 activate 事件中,我们需要检查cacheName是否变化,如果变化则表示有了新的缓存资源,原有缓存需要删除

  • sw.js
self.addEventListener('activate', function (e) {
    console.log('Service Worker 状态: activate');
    var cachePromise = caches.keys().then(function (keys) {
        return Promise.all(keys.map(function (key) {
            if (key !== cacheName) {
                return caches.delete(key);
            }
        }));
    })

    e.waitUntil(cachePromise);

    return self.clients.claim();
});

缓存API数据的 "离线搜索"

Service Worker 除了可以离线访问之外,还能实现 "离线搜索" 的功能

因为 Web App 也会把 XHR 请求的数据缓存一份。而再次请求时,我们会优先使用本地缓存(如果有缓存的话);然后向服务端请求数据,服务端返回数据后,基于该数据替换展示

大致过程如下:

查询接口的缓存与使用策略

首先我们改造一下前一节的代码在 sw.js 的 fetch 事件里进行 API 数据的缓存

  • sw.js
var apiCacheName = 'api-0-1-1';
self.addEventListener('fetch', function (e) {
  // 需要缓存的xhr请求
  var cacheRequestUrls = ['/hitokoto']

  console.log('现在正在请求:' + e.request.url)

  // 判断当前请求是否需要缓存
  var needCache = cacheRequestUrls.some(function (url) {
    return e.request.url.indexOf(url) > -1
  })

  /**** 这里是对XHR数据缓存的相关操作 ****/
  if (needCache) {
    // 需要缓存
    // 使用fetch请求数据,并将请求结果clone一份缓存到cache
    // 此部分缓存后在browser中使用全局变量caches获取
    caches.open(apiCacheName).then(function (cache) {
      return fetch(e.request).then(function (response) {
        cache.put(e.request.url, response.clone())
        return response
      })
    })
  } else {
    /* ******************************* */
    // 非api请求,直接查询cache
    // 如果有cache则直接返回,否则通过fetch请求
    e.respondWith(
      caches
        .match(e.request)
        .then(function (cache) {
          return cache || fetch(e.request)
        })
        .catch(function (err) {
          console.log(err)
          return fetch(e.request)
        }),
    )
  }
})

这里,我们也为 API 缓存的数据创建一个专门的缓存位置,key 值为变量apiCacheName。在fetch事件中,我们首先通过对比当前请求与cacheRequestUrls来判断是否是需要缓存的XHR请求数据,如果是的话,就会使用fetch方法向后端发起请求

fetch.then中我们以请求的 URL 为 key,向 cache 中更新了一份当前请求所返回数据的缓存:cache.put(e.request.url, response.clone())。这里使用clone()方法拷贝一份响应数据,这样我们就可以对响应缓存进行各类操作而不用担心原响应信息被修改了

应用离线 XHR 数据,完成 "离线搜索",提升响应速度

目前为止,我们对 Service Worker(sw.js)的改造已经完毕了。最后只剩下如何在 XHR 请求时有策略的使用缓存了,这一部分的改造全部集中于 index.js,也就是我们的前端脚本

还是回到上一节的这张图:

查询接口的缓存与使用策略

和普通情况不同,这里我们的前端浏览器会首先去尝试获取缓存数据并使用其来渲染界面;同时,浏览器也会发起一个 XHR 请求,Service Worker 通过将请求返回的数据更新到存储中的同时向前端 Web 应用返回数据(这一步分就是上一节提到的缓存策略);最终,如果判断返回的数据与最开始取到的 cache 不一致,则重新渲染界面,否则忽略

为了是代码更清晰,我们将原本的XHR请求部分单独剥离出来,作为一个方法getApiDataRemote()以供调用,同时将其改造为了Promise

  • public/index.js
function getApiDataRemote(url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest()
    xhr.timeout = 60000
    xhr.onreadystatechange = function () {
      var response = {}
      if (xhr.readyState === 4 && xhr.status === 200) {
        try {
          response = JSON.parse(xhr.responseText)
        } catch (e) {
          response = xhr.responseText
        }
        resolve(response)
      } else if (xhr.readyState === 4) {
        resolve()
      }
    }
    xhr.onabort = reject
    xhr.onerror = reject
    xhr.ontimeout = reject
    xhr.open('GET', url, true)
    xhr.send(null)
  })
}

这一节最重要的部分其实是读取缓存。我们知道,在 Service Worker 中是可以通过 caches 变量来访问到缓存对象的。令人高兴的是,在我们的前端应用中,也仍然可以通过 caches 来访问缓存。当然,为了保证兼容性,我们需要先进行判断'caches' in window

为了代码的统一,我将获取该请求的缓存数据也封装成了一个Promise方法:

function getApiDataFromCache(url) {
  if ('caches' in window) {
    return caches.match(url).then(function (cache) {
      if (!cache) {
        return
      }
      return cache.json()
    })
  } else {
    return Promise.resolve()
  }
}

而原本我们在getData()方法中,我们会请求后端数据,然后渲染页面;而现在,我们加上基于缓存的渲染:

function getData() {
  const contentDom = document.getElementById('content')
  var url = 'https://api.wrdan.com/hitokoto'
  var cacheData

  contentDom.innerHTML = ''

  var remotePromise = getApiDataRemote(url)
  getApiDataFromCache(url)
    .then(function (data) {
      if (data) {
        contentDom.innerHTML = data.text
      }

      cacheData = data || {}

      return remotePromise
    })
    .then(function (data) {
      if (data && JSON.stringify(data) !== JSON.stringify(cacheData)) {
        contentDom.innerHTML = data.text
      }
    })
}

如果getApiDataFromCache(url).then返回缓存数据,则使用它先进行渲染。而当remotePromise的数据返回时,与cacheData进行比对,只有在数据不一致时需要重新渲染页面(注意这里为了简便,粗略地使用了JSON.stringify()方法进行对象间的比较)

这么做有两个优势:

  1. 离线可用:如果我们之前访问过某些URL,那么即使在离线的情况下,重复相应的操作依然可以正常展示页面
  2. 优化体验,提高访问速度:读取本地 cache 耗时相比于网络请求是非常低的,因此就会给我们的用户一种 "秒开"、"秒响应" 的感觉

兼容性

Service Workers 兼容性

可以看到一些低版本或IE浏览器不支持,大多数浏览器都是兼容的

本章分支: sw-cache

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/152755.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis哨兵模式搭建

以下配置机器部署ip为 a、b、c,其中a为master节点 需提前创建 /app/user/oms/redis/data 目录 1.1上传 redis-5.0.5.zip 到对应目录,解压 unzip redis-5.0.5.zip # 生成 redis-5.0.5 目录 1.2 修改配置文件 maxclients 10000 #20000 &#xff0…

接口测试实战| GET/POST 请求区别详解

在日常的工作当中,HTTP 请求中使用最多的就是 GET 和 POST 这两种请求方式。深度掌握这两种请求方式的原理以及异同之处,也是之后做接口测试一个重要基础。GET、POST 的区别总结请求行的 method 不同;POST 可以附加 div,可以支持 …

概率论【离散型二维变量与连续性二维变量(下)】--猴博士爱讲课

6.连续型二维变量(下) 1/7 求边缘分布函数 边缘概率密度 边缘概率密度 2/7 求边缘密度函数 边缘概率密度 3/7 判断连续型二维变量的独立性 F(x,y) Fx(X) * Fy(Y)那么X、Y互相独立 f(x,y) fx(X) * fy(Y)那么X、Y互相独立 这种题目带入验证就可以了 先求…

百度举办首个人机共创大会,最强技术天团邀约全球开发者

1月10日,百度举办Create AI开发者大会(下称“Create大会”)。作为首个“人机共创大会”,AIGC(利用AI技术自动生成内容的生产方式)技术被深度应用,创造、搭建、连接了多个科技感爆棚的数字化演讲…

powershell ISE 多个选项卡,替换命令行黑窗口

安装powershell ISE设置权限解决方案1.管理员打开PowerShell2. 执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser运行脚本自定义函数,function start_service([string]$Name,$p,$r) {$NewTab $psISE.PowerShellTabs.Add()$NewTab.Displa…

ES之module

模块:一个一个的局部作用域的代码块 模块系统需要解决的主要问题 模块化的问题消除全局变量管理加载顺序 Module的基本用法 模块里面都是局部无法访问 切换幻灯片示例 Base.js // 默认参数 const DEFAULTS {// 初始索引initialIndex: 0,// 切换时是否有动画a…

Repvgg推理时融合BN

Batch Normalization是谷歌研究员于2015年提出的一种归一化方法,其思想非常简单,一句话概括就是,对一个神经元(或者一个卷积核)的输出减去统计得到的均值,除以标准差,然后乘以一个可学习的系数&…

数字孪生|可视化图表之堆叠面积图

上一篇文章为大家介绍了分组条形图的相关内容,本文介绍的是堆叠面积图。 堆叠面积图是一种特殊的面积图,可以用来比较在一个区间内的多个变量。堆叠面积图和普通的面积图基本一样,唯一的区别就是堆叠面积图每个数据系列的起点都是基于前一个数…

再学C语言30:函数——ANSI C的函数原型

一、ANSI C关于函数原型的规则 ANSI C在函数声明中同事说明所使用的的参数类型,即在函数原型中声明返回值类型、参数、参数个数、参数类型 int function(int a, int b); // 形式一 int function(int, int); // 形式二// 以上两种形式的定义均满足规范要求 好…

基于无线通信物联网的水库安全监测系统

水库安全监测一直是我国防洪防汛工作的重点,库区的雨量、水位的实时监测,建立水库监测系统能够有效防止洪涝灾害的发生,确保水库和下游地区的安全。 物通博联推出的水库安全监测系统是基于无线通信物联网技术打造的系统平台,由雨…

路由器基础

交换机基本功能 1.基于源MAC地址学习2.基于目标MAC地址转发3.数据过滤4.防环 交换机基于MAC地址表进行转发,MAC地址表默认自动产生,MAC地址组成三元组:Mac地址、端口、VLAN。默认MAC条目信息存活时间为300s并且可以修改 泛洪(洪…

局域网主机状态管理工具LanNeighborManager

有鉴于以下两种需求,做了一个小工具,查看局域网内的IP占用情况及联通状态:1)在现场需要分配静态IP,但是不确定已经被占用的IP地址;2)查看当前局域网内当前活跃的IP地址。该工具的主要功能有&…

华宏转债上市价格预测

华宏转债基本信息转债名称:华宏转债,评级:AA-,发行规模:5.15亿元。正股名称:华宏科技,今日收盘价:17.68,转股价格:15.65。当前转股价值 转债面值 / 转股价格…

2023.1.2-1.18 AI行业周刊(第131期):程序员的艰辛和感悟

转眼间,还有两周不到的时间,就要到除夕了。今年的春节和元旦离得很近,只有二十多天的时间。 班上部门的同事,一个个也都开始阳康,回到工作岗位,做春节前产品的最后一次版本迭代。 一晃从去年三月份进入新…

3-计算字符串的编辑距离(华为机试)

题目 Levenshtein 距离,又称编辑距离,指的是两个字符串之间,由一个转换成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。编辑距离的算法是首先由俄国科学…

Bonitasoft认证绕过和RCE漏洞分析及复现(CVE-2022-25237)

一、漏洞原理 漏洞简述 Bonitasoft 是一个业务自动化平台,可以更轻松地在业务流程中构建、部署和管理自动化应用程序; Bonita 是一个用于业务流程自动化和优化的开源和可扩展平台。 Bonita Web 2021.2版本受到认证绕过影响,因为其API认证…

没有为请求的 URL 配置默认文档,并且没有在服务器上启用目录浏览

1、问题 使用asp.net mvc写了个网站&#xff0c;部署后出现&#xff1a; 没有为请求的 URL 配置默认文档&#xff0c;并且没有在服务器上启用目录浏览 这个问题 2、解决办法 网上搜了下&#xff0c;无非是两种方法&#xff1a; 1、在web.config中添加 <system.webServ…

概率论【合集】--猴博士爱讲课

重点章节 条件概率&#xff0c;期望等等 第一课 随机事件和概率 1/6 无放回类题目(一次摸多个) 例1.盒子里有3绿4红共7个小球&#xff0c;无放回的摸3个试求摸出1绿2红的概率例2.钱包里有3张100元&#xff0c;5张10元&#xff0c;3张5元的纸币&#xff0c;随机摸3张&#xff0…

excel图表美化:堆栈式柱状图让趋势变化更明显

平时大家都是怎么做图表的呢&#xff1f;是不是直接在Excel上点击插入图表&#xff0c;就完事儿了&#xff1f;虽然这样也能做出图表&#xff0c;但是必定没什么亮点&#xff0c;老板看久了&#xff0c;自然也就腻了&#xff01;我们每个人都想让自己的的图表看起来漂亮&#x…

BGP基础实验(1.10)

目标: 1、首先为所有路由器配置接口ip和环回 r1&#xff1a; [r1]int lo0 [r1-LoopBack0]ip add 1.1.1.1 24 [r1-LoopBack0]int gi 0/0/0 [r1-GigabitEthernet0/0/0]ip add 12.1.1.1 24 r2&#xff1a; [r2]int lo0 [r2-LoopBack0]ip add 2.2.2.2 24 [r2-LoopBack0]int gi …