渐进式web应用开发--拥抱离线优先(三)
2019-07-17 00:44 龙恩0707 阅读(921) 评论(0) 编辑 收藏 举报阅读目录
一:什么是离线优先?
传统的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