• 渐进式web应用开发--拥抱离线优先(三)


    阅读目录

    一:什么是离线优先?

    传统的web应用完全依赖于服务器端,比如像很早以前jsp,php,asp时代,所有的数据,内容和应用逻辑都在服务器端,客户端仅仅做一些html内容渲染到页面上去。但是随着技术在不断的改变,现在很多业务逻辑也放在前端,前后端分离,前端是做模板渲染工作,后端只做业务逻辑开发,只提供数据接口。但是我们的web前端开发在数据层这方面来讲还是依赖于服务器端。如果网络中断或服务器接口挂掉了,都会影响数据页面展示。因此我们需要使用离线优先这个技术来更优雅的处理这个问题。

    拥抱离线优先的真正含义是:尽管应用程序的某些功能在用户离线时可能不能正常使用,但是更多的功能应该保持可用状态。

    离线优先它可以优雅的处理这些异常情况下问题,当用户离线时,用户正在查看数据可能是之前的数据,但是仍然可以访问之前的页面,之前的数据不会丢失,这就意味着用户可以放心使用某些功能。那么要做到离线时候还可以访问,就需要我们缓存哦。

    二:常用的缓存模式

    在为我们的网站使用缓存之前,我们需要先熟悉一些常见的缓存设计模式。如果我们要做一个股票K线图的话,因为股票数据是实时更新的,因此我们需要实时的去请求网络最新的数据(当然实时肯定使用websocket技术,而不是http请求,我这边是假如)。只有当网络请求失败的时候,我们再从缓存里面去读取数据。但是对于股票K线图中的一些图标展示这样的,因为这些图标是一般不会变的,所以我们更倾向于使用缓存里面的数据。只有在缓存里面找不到的情况下,再从网络上请求数据。

    所以有如下几种缓存模式:

    1. 仅缓存
    2. 缓存优先,网络作为回退方案。
    3. 仅网络。
    4. 网络优先,缓存作为回退方案。
    5. 网络优先,缓存作为回退方案, 通用回退。

    1. 仅缓存

    什么是仅缓存呢?仅缓存是指 从缓存中响应所有的资源请求,如果缓存中找不到的话,那么请求就会失败。那么仅缓存对于静态资源是实用的。因为静态资源一般是不会发生变化,比如图标,或css样式等这些,当然如果css样式发生改变的话,在后缀可以加上时间戳这样的。比如 base.css?t=20191011 这样的,如果时间戳没有发生改变的话,那么我们直接从缓存里面读取。
    因此我们的 sw.js 代码可以写成如下(注意:该篇文章是在上篇文章基础之上的,如果想看上篇文章,请点击这里

    self.addEventListener("fetch", function(event) {
      event.respondWith(
        caches.match(event.request)
      )
    });

    如上代码,直接监听 fetch事件,该事件能监听到页面上所有的请求,当有请求过来的时候,它使用缓存里面的数据依次去匹配当前的请求,如果匹配到了,就拿缓存里面的数据,如果没有匹配到,则请求失败。

    2. 缓存优先,网络作为回退方案

    该模式是:先从缓存里面读取数据,当缓存里面没有匹配到数据的时候,service worker才会去请求网络并返回。

    代码变成如下:

    self.addEventListener("fetch", function(event) {
      event.respondWith(
        caches.match(event.request).then(function(response) {
          return response || fetch(event.request);
        })
      )
    });

    如上代码,使用fetch去监听所有请求,然后先使用缓存依次去匹配请求,不管是匹配成功还是匹配失败都会进入then回调函数,当匹配失败的时候,我们的response值就为 undefined,如果为undefined的话,那么就网络请求,否则的话,从拿缓存里面的数据。

    3. 仅网络

    传统的web模式,就是这种模式,从网络里面去请求,如果网络不通,则请求失败。因此代码变成如下:

    self.addEventListener("fetch", function(event) {
      event.respondWith(
        fetch(event.request)
      )
    });

    4. 网络优先,缓存作为回退方案。

    先从网络发起请求,如果网络请求失败的话,再从缓存里面去匹配数据,如果缓存里面也没有找到的话,那么请求就会失败。

    因此代码如下:

    self.addEventListener("fetch", function(event) {
      event.respondWith(
        fetch(event.request).catch(function() {
          return caches.match(event.request);
        })
      )
    });

    5. 网络优先,缓存作为回退方案, 通用回退

    该模式是先请求网络,如果网络失败的话,则从缓存里面读取,如果缓存里面读取失败的话,我们提供一个默认的显示给页面展示。

    比如显示一张图片。如下代码:

    self.addEventListener("fetch", function(event) {
      event.respondWith(
        fetch(event.request).catch(function() {
          return caches.match(event.request).then(function(response) {
            return response || caches.match("/xxxx.png");
          })
        });
      )
    });

    三:混合与匹配,创造新模式

    上面是我们五种缓存模式。下面我们需要将这些模式要组合起来使用。

    1. 缓存优先,网络作为回退方案, 并更新缓存。

    对于不经常改变的资源,我们可以先缓存优先,网络作为回退方案,第一次请求完成后,我们把请求的数据缓存起来,下次再次执行的时候,我们先从缓存里面读取。

    因此代码如下:

    self.addEventListener("fetch", function(event) {
      event.respondWith(
        caches.open("cache-name").then(function(cache) {
          return cache.match(event.request).then(function(cachedResponse){
            return cachedResponse || fetch(event.request).then(function(networkResponse){
              cache.put(event.request, networkResponse.clone());
              return networkResponse;
            });
          })
        })
      )
    });

    如上代码,我们首先打开缓存,然后使用请求匹配缓存,不管匹配成功了还是匹配失败了,都会进入then回调函数,如果匹配到了,说明缓存里面有对应的数据,那么直接从缓存里面返回,如果缓存里面 cachedResponse 值为undefined,没有的话,那么就重新使用fetch请求网络,然后把请求的数据 networkResponse 重新返回回来,并且克隆一份 networkResponse 放入缓存里面去。

    2. 网络优先,缓存作为回退方案,并频繁更新缓存

    如果一些经常要实时更新的数据的话,比如百度上的一些实时新闻,那么都需要对网络优先,缓存作为回退方案来做,那么该模式下首先会从网络中获取最新版本,当网络请求失败的时候才回退到缓存版本,当网络请求成功的时候,它会将当前返回最新的内容重新赋值给缓存里面去。这样就保证缓存永远是上一次请求成功的数据。即使网络断开了,还是会使用之前最新的数据的。

    因此代码可以变成如下:

    self.addEventListener("fetch", function(event) {
      event.respondWith(
        caches.open("cache-name").then(function(cache) {
          return fetch(event.request).then(function(networkResponse) {
            cache.put(event.request, networkResponse.clone());
            return networkResponse;
          }).catch(function() {
            return caches.match(event.request);
          });
        })
      )
    });

    如上代码,我们使用fetch事件监听所有的请求,然后打开缓存后,我们先请网络请求,请求成功后,返回最新的内容,此时此刻同时把该返回的内容克隆一份放入缓存里面去。但是当网络异常的情况下,我们就匹配缓存里面最新的数据。但是在这种情况下,如果我们第一次网络请求失败后,由于第一次我们没有做缓存,因此缓存也会失败,最后就会显示失败的页面了。

    3. 缓存优先,网络作为回退方案,并频繁更新缓存

    对于一些经常改变的资源文件,我们可以先缓存优先,然后再网络作为回退方案,也就是说先缓存里面找到,也总会从网络上请求资源,这种模式可以先使用缓存快速响应页面,同时会重新请求来获取最新的内容来更新缓存,在我们用户下次请求该资源的时候,那么它就会拿到缓存里面最新的数据了,这种模式是将快速响应和最新的响应模式相结合。

    因此我们的代码改成如下:

    self.addEventListener('fetch', function(event) {
      event.respondWith(
        caches.open("cache-name").then(function(cache) {
          return cache.match(event.request).then(function(cachedResponse) {
            var fetchPromise = fetch(event.request).then(function(networkResponse) {
              cache.put(event.request, networkResponse.clone());
              return networkResponse;
            });
            return cachedResponse || fetchPromise;
          });
        })
      )
    });

    如上代码,我们首先打开一个缓存,然后我们试图匹配请求,不管是否匹配成功,我们都会进入then函数,在该回调函数内部,会先重新请求一下,请求成功后,把最新的内容返回回来,并且以此同时把该请求数的数据克隆一份出来放入缓存里面去。最后把请求的资源文件返回保存到 fetchPromise 该变量里面,最后我们先返回缓存里面的数据,如果缓存里面没有数据,我们再返回网络fetchPromise 返回的数据。

    如上就是我们3种常见的模式。下面我们就需要来规划我们的缓存策略了。

    四:规划缓存策略

    在我们之前讲解的demo中(https://www.cnblogs.com/tugenhua0707/p/11148968.html), 都是基于网络优先,缓存作为回退方案模式的。我们之前使用这个模式给用户体验还是挺不错的,首先先请求网络,当网络断开的时候,我们从缓存里面拿到数据。
    这样就不会使页面异常或空白。但是上面我们已经了解到了缓存了,我们可以再进一步优化了。

    我们现在可以使用离线优先的方式来构建我们的应用程序了,对应我们项目经常会改变的资源我们优先使用网络请求,如果网络不可以用的话,我们使用缓存里面的数据。

    首先还是看下我们项目的整个目录结构如下:

    |----- 项目
    |  |--- public
    |  | |--- js               # 存放所有的js
    |  | | |--- main.js        # js入口文件
    |  | |--- style            # 存放所有的css
    |  | | |--- main.styl      # css 入口文件
    |  | |--- index.html       # index.html 页面
    |  | |--- images
    |  |--- package.json
    |  |--- webpack.config.js
    |  |--- node_modules
    |  |--- sw.js

    我们的首页 index.html 代码如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>service worker 实列</title>
      <link rel="stylesheet" href="/main.css" />
    </head>
    <body>
      <div id="app">22222</div>
      <img src="/public/images/xxx.jpg" />
      <script type="text/javascript" src="/main.js"></script>
    </body>
    </html>

    首页是由静态的index.html 组成的,它一般很少会随着版本的改变而改变的,它页面中会请求多个图片,请求多个css样式,和请求多个js文件。在index.html中所有的静态资源文件(图片、css、js)等在我们的service worker安装过程中会缓存下来的,那么这些资源文件适合的是 "缓存优先,网络作为回退方案" 模式来做。这样的话,页面加载会更快。

    但是index.html呢?这个页面一般情况下很少改变,我们一般会想到 "缓存优先,网络作为回退方案" 来考虑,但是如果该页面也改动了代码呢?我们如果一直使用缓存的话,那么我们就得不到最新的代码了,如果我们想我们的index.html拿到最新的数据,我们不得不重新更新我们的service worker,来获取最新的缓存文件。但是我们从之前的知识点我们知道,在我们旧的service worker 释放页面的同时,新的service worker被激活之前,页面也不是最新的版本的。必须要等第二次重新刷新页面的时候才会看到最新的页面。那么我们的index.html页面要如何做呢?

    1) 如果我们使用 "缓存优先,网络作为回退方案" 模式来提供服务的话,那么这样做的话,当我们改变页面的时候,它就有可能不会使用最新版本的页面。

    2)如果我们使用 "网络优先,缓存作为回退方案 " 模式来做的话,这样确实可以通过请求来显示最新的页面,但是这样做也有缺点,比如我们的index.html页面没有改过任何东西的话,也要从网络上请求,而不是从缓存里面读取,导致加载的时间会慢一点。

    3) 使用 缓存优先,网络作为 回退方案,并频繁更新缓存模式。该模式总是从缓存里面读取 index.html页面,那么它的响应时间相对来说是非常快的,并且从缓存里面读取页面后,我们同时会请求下,然后返回最新的数据,我们把最新的数据来更新缓存,因此我们下一次进来页面的时候,会使用最新的数据。

    因此对于我们的index.html页面,我们适合使用第三种方案来做。

    因此对于我们这个简单的项目来讲,我们可以总结如下:

    1. 使用 "缓存优先,网络作为回退方案,并频繁更新缓存" 模式来返回index.html文件。
    2. 使用 "缓存优先,网络作为回退方案" 来返回首页需要的所有静态文件。

    因此我们可以使用上面两点,来实现我们的缓存策略。

    五:实现缓存策略

    现在我们来更新下我们的 sw.js 文件,该文件来缓存我们index.html,及在index.html使用到的所有静态资源文件。

    index.html 代码改成如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>service worker 实列</title>
    </head>
    <body>
      <div id="app">22222</div>
      <img src="/public/images/xxx.jpg" />
    </body>
    </html>

    js/main.js 代码变为如下:

    // 加载css样式
    require('../styles/main.styl');
    
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register('/sw.js', {scope: '/'}).then(function(registration) {
        console.log("Service Worker registered with scope: ", registration.scope);
      }).catch(function(err) {
        console.log("Service Worker registered failed:", err);
      });
    }

    sw.js 代码变成如下:

    var CACHE_NAME = "cacheName";
    
    var CACHE_URLS = [
      "/public/index.html",      // html文件
      "/main.css",               // css 样式表
      "/public/images/xxx.jpg",  // 图片
      "/main.js"                 // js 文件 
    ];
    
    // 监听 install 事件,把所有的资源文件缓存起来
    self.addEventListener("install", function(event) {
      event.waitUntil(
        caches.open(CACHE_NAME).then(function(cache) {
          return cache.addAll(CACHE_URLS);
        })
      )
    });
    
    // 监听fetch事件,监听所有的请求
    
    self.addEventListener("fetch", function(event) {
      var requestURL = new URL(event.request.url);
      console.log(requestURL);
      if (requestURL.pathname === '/' || requestURL.pathname === "/index.html") {
        event.respondWith(
          caches.open(CACHE_NAME).then(function(cache) {
            return cache.match("/index.html").then(function(cachedResponse) {
              var fetchPromise = fetch("/index.html").then(function(networkResponse) {
                cache.put("/index.html", networkResponse.clone());
                return networkResponse;
              });
              return cachedResponse || fetchPromise;
            })
          })
        )
      } else if (CACHE_URLS.includes(requestURL.href) || CACHE_URLS.includes(requestURL.pathname)) {
        event.respondWith(
          caches.open(CACHE_NAME).then(function(cache) {
            return cache.match(event.request).then(function(response) {
              return response || fetch(event.request);
            });
          })
        )
      } 
    });
    
    self.addEventListener("activate", function(e) {
      e.waitUntil(
        caches.keys().then(function(cacheNames) {
          return Promise.all(
            cacheNames.map(function(cacheName) {
              if (CACHE_NAME !== cacheName && cacheName.startWith("cacheName")) {
                return caches.delete(cacheName);
              }
            })
          )
        })
      )
    });

    如上代码中的fetch事件,var requestURL = new URL(event.request.url);console.log(requestURL); 打印信息如下所示:

    如上我们使用了 new URL(event.request.url) 来决定如何处理不同的请求。且可以获取到不同的属性,比如host, hostname, href, origin 等这样的信息到。

    如上我们监听 fetch 事件中所有的请求,判断 requestURL.pathname 是否是 "/" 或 "/index.html", 如果是index.html 页面的话,对于 index.html 的来说,使用上面的原则是:使用 "缓存优先,网络作为回退方案,并频繁更新缓存", 所以如上代码,我们首先打开我们的缓存,然后使用缓存匹配 "/index.html",不管匹配是否成功,都会进入then回调函数,然后把缓存返回,在该函数内部,我们会重新请求,把请求最新的内容保存到缓存里面去,也就是说更新我们的缓存。当我们第二次访问的时候,使用的是最新缓存的内容。

    如果我们请求的资源文件不是 index.html 的话,我们接着会判断下,CACHE_URLS 中是否包含了该资源文件,如果包含的话,我们就从缓存里面去匹配,如果缓存没有匹配到的话,我们会重新请求网络,也就是说我们对于页面上所有静态资源文件话,使用 "缓存优先,网络作为回退方案" 来返回首页需要的所有静态文件。

    因此我们现在再来访问我们的页面的话,如下所示:

    如上所示,我们可以看到,我们第一次请求的时候,加载index.html 及 其他的资源文件,我们可以从上图可以看到 加载时间的毫秒数,虽然从缓存里面读取第一次数据后,但是由于我们的index.html 总是会请求下,把最新的资源再返回回来,然后更新缓存,因此我们可以看到我们第二次加载index.html 及 所有的service worker中的资源文件,可以看到第二次的加载时间更快,并且当我们修改我们的index.html 后,我们刷新下页面后,第一次还是从缓存里面读取最新的数据,当我们第二次刷新的时候,页面才会显示我们刚刚修改的index.html页面的最新页面了。因此就验证了我们之前对于index.html 处理的逻辑。

    使用 缓存优先,网络作为 回退方案,并频繁更新缓存模式。该模式总是从缓存里面读取 index.html页面,那么它的响应时间相对来说是非常快的,并且从缓存里面读取页面后,我们同时会请求下,然后返回最新的数据,我们把最新的数据来更新缓存,因此我们下一次进来页面的时候,会使用最新的数据。
    github简单的demo

  • 相关阅读:
    Win7 华硕电脑自带摄像头无法打开 方法思路介绍
    P3520 [POI2011]SMI-Garbage
    二分图的最大匹配(模板)
    #数列分块入门 2
    数列分块入门#1
    线段树(标记下传乘法和加法)
    最小费用最大流
    最大流算法(模板)
    编译器出现/mingw32/bin/ld.exe:Permission denied 错误
    1298 圆与三角形
  • 原文地址:https://www.cnblogs.com/tugenhua0707/p/11198509.html
Copyright © 2020-2023  润新知