代码改变世界

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

2019-07-17 00:44 龙恩0707 阅读(...) 评论(...) 编辑 收藏

阅读目录

一:什么是离线优先?

传统的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