回到顶部
一: 什么是离线优先?
传统的 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
来源: https://www.cnblogs.com/tugenhua0707/p/11198509.html