PWA

783 阅读8分钟

Service workers

Service Workers是浏览器和网络之间的虚拟代理。 他们最终解决了前端开发人员多年来一直在努力解决的问题 - 最值得注意的是解决了如何正确缓存网站资源并使其在用户设备离线时可用。

它们的运行在一个与我们页面的 JavaScript 主线程独立的线程上,并且没有对 DOM 结构的任何访问权限。 这引入了与传统 Web 编程不同的方法 - API 是非阻塞的,并且可以在不同的上下文之间发送和接收信息。 您可分配给 Service Worker 一些任务,并在使用基于 Promise 的方法当任务完成时收到结果。

他们不仅仅提供离线功能,还提供包括处理通知,在单独的线程上执行繁重的计算等。Service workers 非常强大,因为他们可以控制网络请求,修改网络请求,返回缓存的自定义响应,或合成响应。

因为它们非常强大,所以 Service Workers 只能在安全的上下文中执行(即 HTTPS )。 如果您想在将代码推送到生产环境之前先进行实验,则可以始终在本地主机上进行测试或设置 GitHub 页面 - 两者都支持HTTPS。

优势

离线优先

“离线优先”或“缓存优先”模式是向用户提供内容的最流行策略。 离线优先允许用户在网络不好的情况下,也能放心的使用这个应用,并且的数据不会丢失。离线优先意味着,总是基于当前网络,获得最佳体验。

常用的缓存模式

下面来看几种常见的缓存模式。

仅缓存

这种模式对静态资源非常实用,代码如下:

self.addEventLister("fetch",
function(event) {
    event.respondWith(caches.match(event.request););
});
缓存优先,网络作为回退方案

这个模式也会从缓存中相应请求。然而,如果在缓存中找不到内容,service worker 会从网络中请求并返回:

self.addEventListener("fetch",
function(event) {
    event.respondWith(caches.match(event.resquest).then((response) = >{
        return response || fetch(event.request);
    }))
});
仅网络

经典的web模型,尝试从网络中请求,网络不通,则请求失败。

self.addEventListener("fetch",
function(event) {
    event.respondWith(fetch(event.request));
});
网络优先,缓存做为回退方案

总是向网络发起请求,请求失败则返回缓存中的版本。

self.addEventListener("fetch",
function(event) {
    event.respondWith(caches.match(event.resquest).
    catch(() = >{
        return caches.match(event.request);
    }))
});
网络优先,缓存作为回退方案,通用回退作为兜底方案
self.addEventListener("fetch",
function(event) {
    event.respondWith(caches.match(event.resquest).
    catch(() = >{
        return caches.match(event.request).then((response) = >{
            return response || caches.match("/generic.png");
        });
    }));
});
按需缓存

对于不经常改变的资源以及service worker install 事件期间不想缓存的资源,可以扩展缓存优先,网络作为回退方案的模式。将网络返回的请求保存到缓存中。

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

在响应保存到缓存中时,对其使用了一个clone方法。

fetch(event.request).then((networkResponse) {
    cache.put(event.request, networkResponse.clone());
    return networkResponse;
});

这是因为打算不止一次使用它(将其放入缓存并使用它来相应事件)。这样就要确保使用clone命令来复制它

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

对于经常修改的资源(如用户头像),可以修改缓存优先,网络作为回退方案的模式。即使在缓存中找到,也总会从缓存中请求资源。

self.addEventListener("fetch",
function(event) {
    event.respondWith(caches.open('cache-name').then((cache) = >{
        return caches.match(event.resquest).then((cachedResponse) = >{
            const fetchPromise = fetch(event.request).then((networkResponse) {
                cache.put(event.request, networkResponse.clone());
                return networkResponse;
            });
            return cachedResponse || fetchPromise;
        });
    }));
});
网络优先,缓存作为回退方案,并频繁更新缓存

该模式总会试图从网络中获取最新版本,仅在网络请求失败的时候才回退到缓存版本。 每当网络访问成功时,会将当前缓存更新为视为网络响应的内容。

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

总结一下缓存策略

1.使用缓存优先,网络作为回退方案,并频繁更新缓存模式返回index.html文件。

2.使用缓存优先,网络作为回退方案,返回首页需要展示的所有静态文件。

3.从网络中返回谷歌地图的javascript文件,如果请求失败,则返回一个代替的脚本。

4.使用网络优先,缓存作为回退方案,并频繁更新缓存模式,返回events.json文件。

5.使用按需缓存模式返回事件的图片文件,如果网络不可用并且图片没有缓存,则退回到默认的通用图片。

6.数据分析请求直接通过,不做处理。

PWA 渐进增强

目的就是在移动端利用提供的标准化框架,在网页应用中实现和Native app原生应用相近的用户体验的渐进式网页应用。它的安全,性能,用户体验的确明显领先于其他互联网载体

优点

可靠

即时加载,即使在不确定的网络条件下也不会受到影响。 当用户从主屏幕启动时,service work可以立即加载渐进式Web应用程序,完全不受网络环境的影响。service work就像一个客户端代理,它控制缓存以及如何响应资源请求逻辑,通过预缓存关键资源,可以消除对网络的依赖,确保为用户提供即时可靠的体验。

快速

据统计,如果站点加载时间超过 3s,53% 的用户会放弃等待。页面展现之后,用户期望有平滑的体验,过渡动画和快速响应。

沉浸式体验

感觉就像设备上的原生应用程序,具有沉浸式的用户体验。 渐进式Web应用程序可以安装并在用户的主屏幕上,无需从应用程序商店下载安装。他们提供了一个沉浸式的全屏幕体验,甚至可以重新与用户接触的Web推送通知。

其他功能

1.无需安装,无需下载,只要你输入网址访问一次,然后将其添加到设备桌面就可以持续使用。

  1. 发布不需要提交到app商店审核

  2. 更新迭代版本不需要审核,不需要重新发布审核

  3. 现有的web网页都能通过改进成为PWA, 能很快的转型,上线,实现业务、获取流量

  4. 不需要开发Android和IOS两套不同的版本

不足的地方
  1. 游览器对技术支持还不够全面, 不是每一款游览器都能100%的支持所有PWA
  2. 需要通过第三方库才能调用底层硬件(如摄像头)
  3. PWA现在还没那么火,国内一些手机生产上在Android系统上做了手脚,似乎屏蔽了PWA, 但是相信当PWA火起来以后,这个问题就不会是问题

js13kPWA应用程序中的 Service workers

注册 Service Worker

首先在 app.js 文件中查看注册新 Service Worker 的代码:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/pwa-examples/js13kpwa/sw.js');
};

如果浏览器支持 service worker API,则使用 ServiceWorkerContainer.register() 方法对该站点进行注册。 其内容在 sw.js 文件中,可以在注册成功后执行。 它是 app.js 文件中唯一的Service Worker代码; 其他关于 Service Worker 的内容都写在 sw.js 文件中。

注册完成后,sw.js 文件会自动下载,然后安装,最后激活。

安装

API 添加事件监听器 - 第一个是 install 事件:

self.addEventListener('install',
function(e) {
    console.log('[Service Worker] Install');
});

在install 的监听函数中, 可以初始化缓存以及添加离线应用时所需的文件。

首先,创建一个作为缓存的名字的变量,app shell所需的文件被记录在一个数组上

var cacheName = 'js13kPWA-v1';
var appShellFiles = [
'/pwa-examples/js13kpwa/',
'/pwa-examples/js13kpwa/index.html',
'/pwa-examples/js13kpwa/app.js',
'/pwa-examples/js13kpwa/style.css',
'/pwa-examples/js13kpwa/fonts/graduate.eot',
'/pwa-examples/js13kpwa/fonts/graduate.ttf',
'/pwa-examples/js13kpwa/fonts/graduate.woff',
'/pwa-examples/js13kpwa/favicon.ico',
'/pwa-examples/js13kpwa/img/js13kgames.png',
'/pwa-examples/js13kpwa/img/bg.png',
'/pwa-examples/js13kpwa/icons/icon-32.png',
'/pwa-examples/js13kpwa/icons/icon-64.png',
'/pwa-examples/js13kpwa/icons/icon-96.png',
'/pwa-examples/js13kpwa/icons/icon-128.png',
'/pwa-examples/js13kpwa/icons/icon-168.png',
'/pwa-examples/js13kpwa/icons/icon-192.png',
'/pwa-examples/js13kpwa/icons/icon-256.png',
'/pwa-examples/js13kpwa/icons/icon-512.png'
];

接下来,从data/games.js的内容中解析出来的图片链接被赋值到另一个数组上,之后,两个数组会用Array.prototype.concat()方法合并起来。

var gamesImages = [];
for (var i = 0; i < games.length; i++) {
    gamesImages.push('data/img/' + games[i].slug + '.jpg');
}
var contentToCache = appShellFiles.concat(gamesImages);

接着监听install事件:

self.addEventListener('install',
function(e) {
    console.log('[Service Worker] Install');
    e.waitUntil(caches.open(cacheName).then(function(cache) {
        console.log('[Service Worker] Caching all: app shell and content');
        return cache.addAll(contentToCache);
    }));
});

ExtendableEvent.waitUntil做了什么 ?caches 对象是什么东西?

service worker会等到 waitUntil 里面的代码执行完毕之后才开始安装。它返回一个promise —— 这个过程时必须的,因为安装过程需要一些时间,必须等待它完成 。

caches 是一个特殊的 CacheStorage 对象,它能在Service Worker指定的范围内提供数据存储的能力(service worker在注册时,第二个参数是选填的,可以被用来指定你想让 service worker 控制的内容的子目录),在service worker中使用web storage 将不会有效果,因为web storage的执行是同步的(此处理解为web storage并不返回一个promise),所以使用Cache API作为替代。

这里,使用给定的名字开启了一个缓存,并且将应用所需要缓存的文件全部添加进去,当再次加载这些资源时,对应的缓存就是可用的(通过请求的url确定缓存是否命中)。

激活

当 Service Worker 安装完成后并进入激活状态,会触发 activate 事件。通过监听 activate 事件你可以做一些预处理,如对旧版本的更新、对无用缓存的清理等。

响应请求

每次当应用发起一个http请求时,还有一个fetch 事件可以使用。这个事件非常有用,它允许拦截请求并对请求作出自定义的响应,下面是一个简单的例子

self.addEventListener('fetch',
function(e) {
    console.log('[Service Worker] Fetched resource ' + e.request.url);
});

请求的响应可以是任何想要的东西:请求过的文件缓存下来的副本,或者一段做了具体操作的JavaScript代码,拥有无限的可能。

当缓存存在时,使用缓存来提供服务而不是重新请求数据。不管当前应用是在线还是离线,都这么做。当请求的文件不在缓存中时,会在响应之前将数据添加到缓存中。

self.addEventListener('fetch',
function(e) {
    e.respondWith(caches.match(e.request).then(function(r) {
        console.log('[Service Worker] Fetching resource: ' + e.request.url);
        return r || fetch(e.request).then(function(response) {
            return caches.open(cacheName).then(function(cache) {
                console.log('[Service Worker] Caching new resource: ' + e.request.url);
                cache.put(e.request, response.clone());
                return response;
            });
        });
    }));
});

上述代码中,对于请求首先会在缓存中查找资源是否被缓存,如果有,将会返回缓存的资源,如果不存在,会转而从网络中请求数据,然后将它缓存起来,这样下次有相同的请求发生时,就可以直接使用缓存。

FetchEvent.respondWith 方法将会接管响应控制,它会作为服务器和应用之间的代理服务。它允许对每一个请求作出想要的任何响应:Service Worker会处理这一切,从缓存中获取这些数据,并在需要的情况下对它们进行修改。

就是这样,应用会在install触发时缓存资源,并且在fetch事件触发时返回缓存中的资源,这就是为什么它甚至在离线状态下也能使用的原因。当添加新的内容时,他也会随时被缓存下来。

还有一点需要考虑:当应用有了一个新版本,并且它包含了一些可用的新资源 时,应该如何去更新它的Service Worker?存放在缓存名称中的版本号是这个问题的关键.

Service Worker 如何更新呢?

service-worker.js控制着页面资源和请求的缓存,如果 js 内容有更新,当访问网站页面时浏览器获取了新的文件,逐字节比对js 文件发现不同时它会认为有更新启动 更新算法,于是会安装新的文件并触发 install 事件。但是此时已经处于激活状态的旧的 Service Worker 还在运行,新的 Service Worker 完成安装后会进入 waiting 状态。直到所有已打开的页面都关闭,旧的 Service Worker 自动停止,新的 Service Worker 才会在接下来重新打开的页面里生效。 如果希望在有了新版本时,所有的页面都得到及时自动更新怎么办呢?可以在 install 事件中执行 self.skipWaiting() 方法跳过 waiting 状态,然后会直接进入 activate 阶段。接着在 activate 事件发生时,通过执行 self.clients.claim() 方法,更新所有客户端上的 Service Worker。

self.addEventListener('install',
function(event) {
    event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate',
function(event) {
    event.waitUntil(Promise.all([
    // 更新客户端
    self.clients.claim(),

    // 清理旧版本
    caches.keys().then(function(cacheList) {
        return Promise.all(cacheList.map(function(cacheName) {
            if (cacheName !== 'cachev1') {
                return caches.delete(cacheName);
            }
        }));
    })]));
});

当js 文件可能会因为浏览器缓存问题,当文件有了变化时,浏览器里还是旧的文件。这会导致更新得不到响应。如遇到该问题,可尝试这么做:在 Web Server 上添加对该文件的过滤规则,不缓存或设置较短的有效期。

或者手动调用update()来更新

navigator.serviceWorker.register('/service-worker.js').then(reg = >{
    // sometime later…
    reg.update();
});

可以结合localStorage来使用,不必每次加载更新

var version = 'v1';
navigator.serviceWorker.register('/service-worker.js').then(function(reg) {
    if (localStorage.getItem('sw_version') !== version) {
        reg.update().then(function() {
            localStorage.setItem('sw_version', version)
        });
    }
});

每个状态都会有ing,进行态。

缓存的清理

self.addEventListener('activate',
function(e) {
    e.waitUntil(caches.keys().then(function(keyList) {
        return Promise.all(keyList.map(function(key) {
            if (cacheName.indexOf(key) === -1) {
                return caches.delete(key);
            }
        }));
    }));
});

PWA存在的问题

局限性

1.支持率不高:现在ios手机端不支持pwa,IE也暂时不支持

2.Chrome在中国桌面版占有率还是不错的,安卓移动端上的占有率却很低

3.各大厂商还未明确支持pwa

4.依赖的GCM服务在国内无法使用

5.微信小程序的竞争

参考文档

第一本PWA中文书 github.com/SangKa/PWA-…

【翻译】Service Worker 入门 www.w3ctech.com/topic/866

微信小程序和PWA对比分析 blog.csdn.net/baidu_brows…

Service Worker最佳实践 x5.tencent.com/tbs/guide/s…