>

渐进式网页应用PWA,离线网页应用

- 编辑:澳门新葡亰平台游戏 -

渐进式网页应用PWA,离线网页应用

3. 使用Service Worker

Service Worker的使用套路是先注册一个Worker,然后后台就会启动一条线程,可以在这条线程启动的时候去加载一些资源缓存起来,然后监听fetch事件,在这个事件里拦截页面的请求,先看下缓存里有没有,如果有直接返回,否则正常加载。或者是一开始不缓存,每个资源请求后再拷贝一份缓存起来,然后下一次请求的时候缓存里就有了。

1. window.navigator

返回一个Navigator对象,该对象简单来说就是允许我们获取我们用户代理(浏览器)的一些信息。比如,浏览器的官方名称,浏览器的版本,网络连接状况,设备位置信息等等。

用户案例

AliExpress一直走在移动商务的前沿,一看到这一领域的增长,他们就迅速开发了自己的渐进式网页应用。他们通过渐进式网页应用获得的新用户转化率与其传统网站和移动应用相比增长了104%,看起来渐进式网页应用对这家中国公司相当适用。

Konga,一个尼日利亚电子商务网站,也在摸索开发渐进式网页应用。他们的目标是减少访问者的数据使用量,因为大约三分之二的尼日利亚用户仍然通过2G上网。据报道,他们的用户平均数据下载量减少了92%,Konga在这方面取得了成功。

eXtra Electronic是一家沙特阿拉伯电子商务公司,他们已经开发了自己的渐进式网页应用, 目标是重新定位现有客户和访客。

最初他们一直在使用电子邮件广告活动推动重新参与策略,但现在他们已经添加了推送通知功能。他们发现用户重新参与度增加了4倍、点击率(click-through rate)增加了12%、销售额增加了100%,而这些用户就来源于推送通知。

使用 Service Worker 做一个 PWA 离线网页应用

2017/10/09 · JavaScript · PWA, Service Worker

原文出处: 人人网FED博客   

在上一篇《我是怎样让网站用上HTML5 Manifest》介绍了怎么用Manifest做一个离线网页应用,结果被广大网友吐槽说这个东西已经被deprecated,移出web标准了,现在被Service Worker替代了,不管怎么样,Manifest的一些思想还是可以借用的。笔者又将网站升级到了Service Worker,如果是用Chrome等浏览器就用Service Worker做离线缓存,如果是Safari浏览器就还是用Manifest,读者可以打开这个网站感受一下,断网也是能正常打开。

5. Activated

如果激活成功,SW 进入激活状态。在此状态中,SW开始接管控制客户端,并可以处理fetch(捕捉请求)、 push(消息推送)、 sync(同步事件)等功能性事件:

// sw.js

self.addEventListener('fetch', function(event) {  
  // Do stuff with fetch events
});

self.addEventListener('message', function(event) {  
  // Do stuff with postMessages received from document
}); 
......

你的公司会受益于渐进式网页应用吗?

渐进式网页应用(Progressive Web Apps,简称PWA)是一个新的概念,它弥合了网站(Website)和移动应用(Mobile App)之间的差异。它们能够确保离线功能的可用性,并且能够提升速度和性能”。

企业采用这项技术有诸多好处。事实上,渐进式网页应用使Google、AliExpress和FlipKart的留存率(retention rate)和转化率(conversion rate)提高了50%~100%。下面我们将了解到,渐进式网页应用通过结合两方面的优势,如何改进用户体验、加强访问者的参与度和提高转化率。

(3)fetch资源后cache起来

如下代码,监听fetch事件做些处理:

JavaScript

this.addEventListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { return response; } return util.fetchPut(event.request.clone()); }) ); });

1
2
3
4
5
6
7
8
9
10
11
12
this.addEventListener("fetch", function(event) {
    event.respondWith(
        caches.match(event.request).then(response => {
            // cache hit
            if (response) {
                return response;
            }
            return util.fetchPut(event.request.clone());
        })
    );
});

先调caches.match看一下缓存里面是否有了,如果有直接返回缓存里的response,否则的话正常请求资源并把它放到cache里面。放在缓存里资源的key值是Request对象,在match的时候,需要请求的url和header都一致才是相同的资源,可以设定第二个参数ignoreVary:

JavaScript

caches.match(event.request, {ignoreVary: true})

1
caches.match(event.request, {ignoreVary: true})

表示只要请求url相同就认为是同一个资源。

上面代码的util.fetchPut是这样实现的:

JavaScript

let util = { fetchPut: function (request, callback) { return fetch(request).then(response => { // 跨域的资源直接return if (!response || response.status !== 200 || response.type !== "basic") { return response; } util.putCache(request, response.clone()); typeof callback === "function" && callback(); return response; }); }, putCache: function (request, resource) { // 后台不要缓存,preview链接也不要缓存 if (request.method === "GET" && request.url.indexOf("wp-admin") < 0 && request.url.indexOf("preview_id") < 0) { caches.open(CACHE_NAME).then(cache => { cache.put(request, resource); }); } } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let util = {
    fetchPut: function (request, callback) {
        return fetch(request).then(response => {
            // 跨域的资源直接return
            if (!response || response.status !== 200 || response.type !== "basic") {
                return response;
            }
            util.putCache(request, response.clone());
            typeof callback === "function" && callback();
            return response;
        });
    },
    putCache: function (request, resource) {
        // 后台不要缓存,preview链接也不要缓存
        if (request.method === "GET" && request.url.indexOf("wp-admin") < 0
              && request.url.indexOf("preview_id") < 0) {
            caches.open(CACHE_NAME).then(cache => {
                cache.put(request, resource);
            });
        }
    }
};

需要注意的是跨域的资源不能缓存,response.status会返回0,如果跨域的资源支持CORS,那么可以把request的mod改成cors。如果请求失败了,如404或者是超时之类的,那么也直接返回response让主页面处理,否则的话说明加载成功,把这个response克隆一个放到cache里面,然后再返回response给主页面线程。注意能放缓存里的资源一般只能是GET,通过POST获取的是不能缓存的,所以要做个判断(当然你也可以手动把request对象的method改成get),还有把一些个人不希望缓存的资源也做个判断。

这样一旦用户打开过一次页面,Service Worker就安装好了,他刷新页面或者打开第二个页面的时候就能够把请求的资源一一做缓存,包括图片、CSS、JS等,只要缓存里有了不管用户在线或者离线都能够正常访问。这样我们自然会有一个问题,这个缓存空间到底有多大?上一篇我们提到Manifest也算是本地存储,PC端的Chrome是5Mb,其实这个说法在新版本的Chrome已经不准确了,在Chrome 61版本可以看到本地存储的空间和使用情况:

图片 1

其中Cache Storage是指Service Worker和Manifest占用的空间大小和,上图可以看到总的空间大小是20GB,几乎是unlimited,所以基本上不用担心缓存会不够用。

1. Parsed

SW是一个JS文件,如果我们要使用一个SW(Service Worker),那么我们需要在我们的js代码中注册它,类似于:
navigator.serviceWorker.register('/sw-1.js')

现在并不需要知道这个方法各个部分的详细含义,只要知道我们现在在为我们的网页注册一个SW就可以了。

可以看到我们传入的参数是一个JS文件的路径,当浏览器执行到这里的时候,就会到相应的路径下载该文件,然后对该脚本进行解析,如果下载或者解析失败,那么这个SW就会被舍弃。

如果解析成功了,那就到了parsed状态。可以进行下面的工作了。

能在设备上快速安装

另外很有意思的一点是在于,当用户访问网站时,一些浏览器会自动提示用户安装渐进式网页应用。这是通过浏览器自身所实现的唤起行动(call to action)来实现的。这使得渐进式网页应用更可信,同时增值了它的权威性和可靠性。

与移动应用相比,用户安装渐进式网页应用时无需很长的下载时间。同时,用户不会被转到Google Play或App Store,而是直接将应用程序下载到他们的设备上。

这意味着渐进式网页应用就像移动应用一样,在手机和平板电脑上有自己的图标,但无需经历乏味和缓慢的应用商店提交过程。

(2)Service Worker安装和激活

注册完之后,Service Worker就会进行安装,这个时候会触发install事件,在install事件里面可以缓存一些资源,如下sw-3.js:

JavaScript

const CACHE_NAME = "fed-cache"; this.addEventListener("install", function(event) { this.skipWaiting(); console.log("install service worker"); // 创建和打开一个缓存库 caches.open(CACHE_NAME); // 首页 let cacheResources = ["]; event.waitUntil( // 请求资源并添加到缓存里面去 caches.open(CACHE_NAME).then(cache => { cache.addAll(cacheResources); }) ); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const CACHE_NAME = "fed-cache";
this.addEventListener("install", function(event) {
    this.skipWaiting();
    console.log("install service worker");
    // 创建和打开一个缓存库
    caches.open(CACHE_NAME);
    // 首页
    let cacheResources = ["https://fed.renren.com/?launcher=true"];
    event.waitUntil(
        // 请求资源并添加到缓存里面去
        caches.open(CACHE_NAME).then(cache => {
            cache.addAll(cacheResources);
        })
    );
});

通过上面的操作,创建和添加了一个缓存库叫fed-cache,如下Chrome控制台所示:

图片 2

Service Worker的API基本上都是返回Promise对象避免堵塞,所以要用Promise的写法。上面在安装Service Worker的时候就把首页的请求给缓存起来了。在Service Worker的运行环境里面它有一个caches的全局对象,这个是缓存的入口,还有一个常用的clients的全局对象,一个client对应一个标签页。

在Service Worker里面可以使用fetch等API,它和DOM是隔离的,没有windows/document对象,无法直接操作DOM,无法直接和页面交互,在Service Worker里面无法得知当前页面打开了、当前页面的url是什么,因为一个Service Worker管理当前打开的几个标签页,可以通过clients知道所有页面的url。还有可以通过postMessage的方式和主页面互相传递消息和数据,进而做些控制。

install完之后,就会触发Service Worker的active事件:

JavaScript

this.addEventListener("active", function(event) { console.log("service worker is active"); });

1
2
3
this.addEventListener("active", function(event) {
    console.log("service worker is active");
});

Service Worker激活之后就能够监听fetch事件了,我们希望每获取一个资源就把它缓存起来,就不用像上一篇提到的Manifest需要先生成一个列表。

你可能会问,当我刷新页面的时候不是又重新注册安装和激活了一个Service Worker?虽然又调了一次注册,但并不会重新注册,它发现”sw-3.js”这个已经注册了,就不会再注册了,进而不会触发install和active事件,因为当前Service Worker已经是active状态了。当需要更新Service Worker时,如变成”sw-4.js”,或者改变sw-3.js的文本内容,就会重新注册,新的Service Worker会先install然后进入waiting状态,等到重启浏览器时,老的Service Worker就会被替换掉,新的Service Worker进入active状态,如果不想等到重新启动浏览器可以像上面一样在install里面调skipWaiting:

JavaScript

this.skipWaiting();

1
this.skipWaiting();

图片 3

不列在应用商店目录中

有些人可能会认为自己的渐进式网页应用没有列在应用商店中会降低曝光率,但通常情况并非如此。

事实上,与移动应用相比,渐进式网页应用可以通过Google或其他搜索引擎上搜索到,这与网站类似,而与移动应用有所不同。这意味着数十亿的日常搜索可能最终导致搜索到渐进式网页应用。

2. Service Worker的支持情况

Service Worker目前只有Chrome/Firfox/Opera支持:

图片 4

Safari和Edge也在准备支持Service Worker,由于Service Worker是谷歌主导的一项标准,对于生态比较封闭的Safari来说也是迫于形势开始准备支持了,在Safari TP版本,可以看到:

图片 5

在实验功能(Experimental Features)里已经有Service Worker的菜单项了,只是即使打开也是不能用,会提示你还没有实现:

图片 6

但不管如何,至少说明Safari已经准备支持Service Worker了。另外还可以看到在今年2017年9月发布的Safari 11.0.1版本已经支持WebRTC了,所以Safari还是一个上进的孩子。

Edge也准备支持,所以Service Worker的前景十分光明。

4. Activating

处于 activating 状态期间,SW 脚本中的 activate 事件被执行。我们通常在 activate 事件中,清理 cache 中的文件(清除旧Worker的缓存文件)。

SW激活失败,则直接进入废弃(redundant)状态。

有限的本地硬件支持

与移动应用本地化设计不同,渐进式网页应用不能100%支持给定手机上的所有硬件功能。

虽然渐进式网页应用支持普通访问的功能,如加速器(Accelerometer)、摄像机和麦克风,但有一些功能需要由本机的移动应用来完成。

1. 什么是Service Worker

Service Worker是谷歌发起的实现PWA(Progressive Web App)的一个关键角色,PWA是为了解决传统Web APP的缺点:

(1)没有桌面入口

(2)无法离线使用

(3)没有Push推送

那Service Worker的具体表现是怎么样的呢?如下图所示:

图片 7

Service Worker是在后台启动的一条服务Worker线程,上图我开了两个标签页,所以显示了两个Client,但是不管开多少个页面都只有一个Worker在负责管理。这个Worker的工作是把一些资源缓存起来,然后拦截页面的请求,先看下缓存库里有没有,如果有的话就从缓存里取,响应200,反之没有的话就走正常的请求。具体来说,Service Worker结合Web App Manifest能完成以下工作(这也是PWA的检测标准):

图片 8

包括能够离线使用、断网时返回200、能提示用户把网站添加一个图标到桌面上等。

二、 register 注册

首先要注册一个SW,在index.js文件中:

// index.js

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    // 注册一个service worker,这个例子中worker的路径是根目录中的,所以这个worker可以缓存这个项目中任意文件。如果目录是‘/js/sw.js‘,那么只能缓存目录'/js'下的文件
    // 参数registration存储了本次注册的一些相关信息
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      // registration.scope 返回的是这个service worker的作用域
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
      // registration failed :(
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

渐进式网页应用的优点:

离线模式

给人的感觉是应用,但运行机制是网站

提高性能

能在设备上快速安装

推送通知 (push notifications)

不必提交到应用软件商店(App Store)

离线模式

网站在某些情况下是有局限性的,在涉及到互联网连接的时候更是如此;没有网络连接时,网站即便能够显示出来,也不可能正常运行。另一方面,移动应用通常是自包含的(self-contained),这方便用户离线浏览,从而大大增加了用户参与度和软件可用性。

这是通过保存访问者已访问过的信息来实现的。这意味着任何时候,即使是没有连接网络的时候,访问者都可以访问渐进式网页应用已访问过的页面。

在没有网络连接的情况下,当用户浏览到先前未访问过的页面时,不是在浏览器中提示错误信息,而是可能显示一个定制的离线页面。该页面可能会显示品牌Logo和基本信息,有时甚至是更先进的功能,旨在吸引用户停留在该页面上。

很明显,这样做的好处在于增加了访客留在该网站上的可能性,而不是促使用户关闭浏览器,等有了网络连接再继续使用。

这已经成为移动应用大幅增长的主要原因之一,催生了一个达数十亿美元的行业。但现在渐进式网页应用正通过帮助普通网站为所有设备实现离线功能,慢慢蚕食这部分市场。

渐进式网页应用对于某些商业模式可能没有财务意义。例如,依赖诸如Google AdSense等服务的网站可能不会感兴趣,因为访问者无法在上面点击广告。但电子商务商店显而易见是渐进式网页应用可以发挥所长的平台。

因访问者在离线模式下也可以访问产品目录,这使得企业有大幅提高他们的客户留存率和参与度的可能。尤其在那些按数据使用量来支付网络费用的国家中,允许用户以离线模式浏览网页,可能对用户来说是个额外的刺激:对比其他商家,用户更可能选择有渐进式网页应用的商家。

给人的感觉是应用,但运行机制是网站

渐进式网页应用的主要卖点在于外观和体验通常会类似于移动应用,让用户在熟悉的环境下操作,同时仍然具有动态数据和数据库访问的全部网站功能。

虽然如何进行渐进式网页应用的设计和编程由每个开发人员自行决定,但是鉴于移动应用比网站更能提供优越的用户体验,因此大多数人还是会完全采用移动应用的现有框架和常规理论。

像网站一样,渐进式网页应用可以通过URL访问,因此可以通过搜索引擎进行索引。这意味着可以在搜索引擎,比如Google和Bing上找到该页面。与所有内部数据只能局限于内部访问的移动应用相比,这是一个巨大的优势。

根据项目要求,渐进式网页应用可以设计成与现有的企业网站或移动应用完全相同,也可以有意设计成有所不同以便让用户感知他们正在浏览渐进式网页应用。甚至可以将渐进式网页应用无缝地集成到现有的网站/应用程序的结构和设计中。

感谢2016年7月的Google研究。

在Google进行的同一项研究中,我们发现所有网站访问者中有11.5%接受并下载了相应的渐进式网页应用。这对任何类型的网站来说都是很高的转化率,并且超过大多数电子邮件新闻注册和电子商务购买的转化率。

结合上述统计数据和接受推送通知的用户数量,我们最终确定转化率在6-7%左右,这对现有网站流量来说仍然是一个可喜的数字。

提高性能

渐进式网页应用的速度要明显快得多,这要归功于底层技术能够缓存和提供文本、样式表、图片以及Web站点上的其他内容。

这得益于服务工作者(service worker),它们的运行独立于Web站点,只请求原始数据,而不涉及任何样式或布局信息。

显然,速度的提升可以改善用户体验和提高留存率。同时,很多报告显示优化性能也能大大提高转化率,这可从销售角度来说增加了渐进式网页应用的价值。

感谢2016年7月的Google研究。(controlled指的是由服务工作者控制页面;supported指的是浏览器支持服务工作者,但是服务工作者没有控制页面。)

上面的图表显示了安装服务工作者,并控制页面内容加载之后,能够明显缩短加载时间。

第一个表格显示的是桌面用户的加载时间。用户使用服务工作者加载网页的时间与使用浏览器加载缓存内容的时间相比减少了29%。

对移动设备而言,性能仍然有明显提高,虽然不及桌面应用,但加载时间还是减少了22%。

值得注意的是,在两种测试中的第三行都基于首次访问的数据,因此无论是否安装服务工作者,结果是一样的 。这是因为服务工作者只有在二次访问时才起作用。

5. 使用Web App Manifest添加桌面入口

注意这里说的是另外一个Manifest,这个Manifest是一个json文件,用来放网站icon名称等信息以便在桌面添加一个图标,以及制造一种打开这个网页就像打开App一样的效果。上面一直说的Manifest是被废除的Application Cache的Manifest。

这个Maifest.json文件可以这么写:

JavaScript

{ "short_name": "人人FED", "name": "人人网FED,专注于前端技术", "icons": [ { "src": "/html/app-manifest/logo_48.png", "type": "image/png", "sizes": "48x48" }, { "src": "/html/app-manifest/logo_96.png", "type": "image/png", "sizes": "96x96" }, { "src": "/html/app-manifest/logo_192.png", "type": "image/png", "sizes": "192x192" }, { "src": "/html/app-manifest/logo_512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": "/?launcher=true", "display": "standalone", "background_color": "#287fc5", "theme_color": "#fff" }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "short_name": "人人FED",
  "name": "人人网FED,专注于前端技术",
  "icons": [
    {
      "src": "/html/app-manifest/logo_48.png",
      "type": "image/png",
      "sizes": "48x48"
    },
    {
      "src": "/html/app-manifest/logo_96.png",
      "type": "image/png",
      "sizes": "96x96"
    },
    {
      "src": "/html/app-manifest/logo_192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "/html/app-manifest/logo_512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/?launcher=true",
  "display": "standalone",
  "background_color": "#287fc5",
  "theme_color": "#fff"
}

icon需要准备多种规格,最大需要512px * 512px的,这样Chrome会自动去选取合适的图片。如果把display改成standalone,从生成的图标打开就会像打开一个App一样,没有浏览器地址栏那些东西了。start_url指定打开之后的入口链接。

然后添加一个link标签指向这个manifest文件:

JavaScript

<link rel="manifest" href="/html/app-manifest/manifest.json">

1
<link rel="manifest" href="/html/app-manifest/manifest.json">

这样结合Service Worker缓存:
图片 9把start_url指向的页面用Service Worker缓存起来,这样当用户用Chrome浏览器打开这个网页的时候,Chrome就会在底部弹一个提示,询问用户是否把这个网页添加到桌面,如果点“添加”就会生成一个桌面图标,从这个图标点进去就像打开一个App一样。感受如下:

图片 10

比较尴尬的是Manifest目前只有Chrome支持,并且只能在安卓系统上使用,IOS的浏览器无法添加一个桌面图标,因为IOS没有开放这种API,但是自家的Safari却又是可以的。

综上,本文介绍了怎么用Service Worker结合Manifest做一个PWA离线Web APP,主要是用Service Worker控制缓存,由于是写JS,比较灵活,还可以与页面进行通信,另外通过请求页面的更新时间来判断是否需要更新html缓存。Service Worker的兼容性不是特别好,但是前景比较光明,浏览器都在准备支持。现阶段可以结合offline cache的Manifest做离线应用。

相关阅读:

  1. 为什么要把网站升级到HTTPS
  2. 怎样把网站升级到http/2
  3. 我是怎样让网站用上HTML5 Manifest

1 赞 1 收藏 评论

图片 11

参考资料
MDN --- Service Worker API
Service Workers: an Introduction
服务工作线程生命周期
Service Worker Cookbook(收集了Service Worker的一些实践例子)
理解 Service Workers

总结

总而言之,渐进式网页应用不会取代网站或移动应用。相反,渐进式网页应用已经在弥合两者之间的差异方面做得很好。有些企业可能需要一个完整的移动应用来实现完整的功能,而其他企业可能只需要一个标准的网站。

渐进式网页应用提供了两方面的最佳途径,如果我们要捕捉和留住没有网络连接的用户,无需提交应用商店就可以推送通知,以及为企业寻找最新的竞争优势,那可能只有它能做到这一点了。

切记,这是一种相对较新的技术,正处于开发和研究的早期阶段。可以将这项技术与当年的开发大西部类比,人们正在推动极限,每天都对新的领域进行探索。

如果不是因为谷歌正在积极寻找成功案例以便在自己网站上展示的话,在任何领域推出第一个渐进式网页应用都会有巨大的市场潜力。

如果您想了解这项新技术的更多信息,请访问以下链接:

Google PWA Study (2016年7月)

Google developer pages

Develop your first progressive web app

4. Http/Manifest/Service Worker三种cache的关系

要缓存可以使用三种手段,使用Http Cache设置缓存时间,也可以用Manifest的Application Cache,还可以用Service Worker缓存,如果三者都用上了会怎么样呢?

会以Service Worker为优先,因为Service Worker把请求拦截了,它最先做处理,如果它缓存库里有的话直接返回,没有的话正常请求,就相当于没有Service Worker了,这个时候就到了Manifest层,Manifest缓存里如果有的话就取这个缓存,如果没有的话就相当于没有Manifest了,于是就会从Http缓存里取了,如果Http缓存里也没有就会发请求去获取,服务端根据Http的etag或者Modified Time可能会返回304 Not Modified,否则正常返回200和数据内容。这就是整一个获取的过程。

所以如果既用了Manifest又用Service Worker的话应该会导致同一个资源存了两次。但是可以让支持Service Worker的浏览器使用Service Worker,而不支持的使用Manifest.

1. cache

Cache是允许我们管理缓存的 Request / Response 对象对的接口,可以通过这个接口增删查改 Request / Response 对。

上面代码中cache.addAll(urlsToCache)表示把数组中的文件都缓存在内存中。
详细了解请戳 : Cache

不必提交应用软件商店

随着需遵守的监管点不断增加,在Google Play、Windows Phone Apps或Apple App Store发布应用程序可能是一个乏味和耗时的过程。

通过使用渐进式网页应用,开发人员无需等待批准就可以推送新的更新,并且能在传统移动应用目前无法实现的级别上进行定期更新。

用户重新运行渐进式网页应用时,系统会自动下载更新。并且,可以通过推送通知,让用户获知应用更新已下载。而且,这同样不是强制性的,软件发布商可以完全控制将什么内容和信息推送给用户。

(1)注册一个Service Worker

Service Worker对象是在window.navigator里面,如下代码:

JavaScript

window.addEventListener("load", function() { console.log("Will the service worker register?"); navigator.serviceWorker.register('/sw-3.js') .then(function(reg){ console.log("Yes, it did."); }).catch(function(err) { console.log("No it didn't. This happened: ", err) }); });

1
2
3
4
5
6
7
8
9
window.addEventListener("load", function() {
    console.log("Will the service worker register?");
    navigator.serviceWorker.register('/sw-3.js')
    .then(function(reg){
        console.log("Yes, it did.");
    }).catch(function(err) {
        console.log("No it didn't. This happened: ", err)
    });
});

在页面load完之后注册,注册的时候传一个js文件给它,这个js文件就是Service Worker的运行环境,如果不能成功注册的话就会抛异常,如Safari TP虽然有这个对象,但是会抛异常无法使用,就可以在catch里面处理。这里有个问题是为什么需要在load事件启动呢?因为你要额外启动一个线程,启动之后你可能还会让它去加载资源,这些都是需要占用CPU和带宽的,我们应该保证页面能正常加载完,然后再启动我们的后台线程,不能与正常的页面加载产生竞争,这个在低端移动设备意义比较大。

还有一点需要注意的是Service Worker和Cookie一样是有Path路径的概念的,如果你设定一个cookie假设叫time的path=/page/A,在/page/B这个页面是不能够获取到这个cookie的,如果设置cookie的path为根目录/,则所有页面都能获取到。类似地,如果注册的时候使用的js路径为/page/sw.js,那么这个Service Worker只能管理/page路径下的页面和资源,而不能够处理/api路径下的,所以一般把Service Worker注册到顶级目录,如上面代码的”/sw-3.js”,这样这个Service Worker就能接管页面的所有资源了。

温馨提示

面临的困难

缺乏通用支持

以下有些重要信息需要注意,主要是并非所有浏览器都支持渐进式网页应用。

Google Chrome和Opera这两个浏览器对服务工作者和渐进式网页应用给与了极大的支持。

苹果的Safari浏览器目前仍然不提供渐进式网页应用支持,虽然有消息说他们会考虑,但迄今为止没有任何具体的内容发布。

微软表示他们将在2016年7月之前在Edge上实施渐进式网页应用,但目前仍然没有关于这方面的消息。

然而,即使不是所有的浏览器都支持渐进式网页应用,对不兼容浏览器的用户也不会造成任何问题,因为这些浏览器只是忽略了渐进式网页应用,依然能够像往常一样显示网站。

(4)cache html

上面第(3)步把图片、js、css缓存起来了,但是如果把页面html也缓存了,例如把首页缓存了,就会有一个尴尬的问题——Service Worker是在页面注册的,但是现在获取页面的时候是从缓存取的,每次都是一样的,所以就导致无法更新Service Worker,如变成sw-5.js,但是PWA又要求我们能缓存页面html。那怎么办呢?谷歌的开发者文档它只是提到会存在这个问题,但并没有说明怎么解决这个问题。这个的问题的解决就要求我们要有一个机制能知道html更新了,从而把缓存里的html给替换掉。

Manifest更新缓存的机制是去看Manifest的文本内容有没有发生变化,如果发生变化了,则会去更新缓存,Service Worker也是根据sw.js的文本内容有没有发生变化,我们可以借鉴这个思想,如果请求的是html并从缓存里取出来后,再发个请求获取一个文件看html更新时间是否发生变化,如果发生变化了则说明发生更改了,进而把缓存给删了。所以可以在服务端通过控制这个文件从而去更新客户端的缓存。如下代码:

JavaScript

this.addEventListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { //如果取的是html,则看发个请求看html是否更新了 if (response.headers.get("Content-Type").indexOf("text/html") >= 0) { console.log("update html"); let url = new URL(event.request.url); util.updateHtmlPage(url, event.request.clone(), event.clientId); } return response; } return util.fetchPut(event.request.clone()); }) ); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.addEventListener("fetch", function(event) {
 
    event.respondWith(
        caches.match(event.request).then(response => {
            // cache hit
            if (response) {
                //如果取的是html,则看发个请求看html是否更新了
                if (response.headers.get("Content-Type").indexOf("text/html") >= 0) {
                    console.log("update html");
                    let url = new URL(event.request.url);
                    util.updateHtmlPage(url, event.request.clone(), event.clientId);
                }
                return response;
            }
 
            return util.fetchPut(event.request.clone());
        })
    );
});

通过响应头header的content-type是否为text/html,如果是的话就去发个请求获取一个文件,根据这个文件的内容决定是否需要删除缓存,这个更新的函数util.updateHtmlPage是这么实现的:

JavaScript

let pageUpdateTime = { }; let util = { updateHtmlPage: function (url, htmlRequest) { let pageName = util.getPageName(url); let jsonRequest = new Request("/html/service-worker/cache-json/" + pageName + ".sw.json"); fetch(jsonRequest).then(response => { response.json().then(content => { if (pageUpdateTime[pageName] !== content.updateTime) { console.log("update page html"); // 如果有更新则重新获取html util.fetchPut(htmlRequest); pageUpdateTime[pageName] = content.updateTime; } }); }); }, delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache "

  • url); cache.delete(url, {ignoreVary: true}); }); } };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let pageUpdateTime = {
 
};
let util = {
    updateHtmlPage: function (url, htmlRequest) {
        let pageName = util.getPageName(url);
        let jsonRequest = new Request("/html/service-worker/cache-json/" + pageName + ".sw.json");
        fetch(jsonRequest).then(response => {
            response.json().then(content => {
                if (pageUpdateTime[pageName] !== content.updateTime) {
                    console.log("update page html");
                    // 如果有更新则重新获取html
                    util.fetchPut(htmlRequest);
                    pageUpdateTime[pageName] = content.updateTime;
                }
            });
        });
    },
    delCache: function (url) {
        caches.open(CACHE_NAME).then(cache => {
            console.log("delete cache " + url);
            cache.delete(url, {ignoreVary: true});
        });
    }
};

代码先去获取一个json文件,一个页面会对应一个json文件,这个json的内容是这样的:

JavaScript

{"updateTime":"10/2/2017, 3:23:57 PM","resources": {img: [], css: []}}

1
{"updateTime":"10/2/2017, 3:23:57 PM","resources": {img: [], css: []}}

里面主要有一个updateTime的字段,如果本地内存没有这个页面的updateTime的数据或者是和最新updateTime不一样,则重新去获取 html,然后放到缓存里。接着需要通知页面线程数据发生变化了,你刷新下页面吧。这样就不用等用户刷新页面才能生效了。所以当刷新完页面后用postMessage通知页面:

JavaScript

let util = { postMessage: async function (msg) { const allClients = await clients.matchAll(); allClients.forEach(client => client.postMessage(msg)); } }; util.fetchPut(htmlRequest, false, function() { util.postMessage({type: 1, desc: "html found updated", url: url.href}); });

1
2
3
4
5
6
7
8
9
let util = {
    postMessage: async function (msg) {
        const allClients = await clients.matchAll();
        allClients.forEach(client => client.postMessage(msg));
    }
};
util.fetchPut(htmlRequest, false, function() {
    util.postMessage({type: 1, desc: "html found updated", url: url.href});
});

并规定type: 1就表示这是一个更新html的消息,然后在页面监听message事件:

JavaScript

if("serviceWorker" in navigator) { navigator.serviceWorker.addEventListener("message", function(event) { let msg = event.data; if (msg.type === 1 && window.location.href === msg.url) { console.log("recv from service worker", event.data); window.location.reload(); } }); }

1
2
3
4
5
6
7
8
9
if("serviceWorker" in navigator) {
    navigator.serviceWorker.addEventListener("message", function(event) {
        let msg = event.data;
        if (msg.type === 1 && window.location.href === msg.url) {
            console.log("recv from service worker", event.data);
            window.location.reload();
        }  
    });
}

然后当我们需要更新html的时候就更新json文件,这样用户就能看到最新的页面了。或者是当用户重新启动浏览器的时候会导致Service Worker的运行内存都被清空了,即存储页面更新时间的变量被清空了,这个时候也会重新请求页面。

需要注意的是,要把这个json文件的http cache时间设置成0,这样浏览器就不会缓存了,如下nginx的配置:

JavaScript

location ~* .sw.json$ { expires 0; }

1
2
3
location ~* .sw.json$ {
    expires 0;
}

因为这个文件是需要实时获取的,不能被缓存,firefox默认会缓存,Chrome不会,加上http缓存时间为0,firefox也不会缓存了。

还有一种更新是用户更新的,例如用户发表了评论,需要在页面通知service worker把html缓存删了重新获取,这是一个反过来的消息通知:

JavaScript

if ("serviceWorker" in navigator) { document.querySelector(".comment-form").addEventListener("submit", function() { navigator.serviceWorker.controller.postMessage({ type: 1, desc: "remove html cache", url: window.location.href} ); } }); }

1
2
3
4
5
6
7
8
9
10
if ("serviceWorker" in navigator) {
    document.querySelector(".comment-form").addEventListener("submit", function() {
            navigator.serviceWorker.controller.postMessage({
                type: 1,
                desc: "remove html cache",
                url: window.location.href}
            );
        }
    });
}

Service Worker也监听message事件:

JavaScript

const messageProcess = { // 删除html index 1: function (url) { util.delCache(url); } }; let util = { delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache "

  • url); cache.delete(url, {ignoreVary: true}); }); } }; this.addEventListener("message", function(event) { let msg = event.data; console.log(msg); if (typeof messageProcess[msg.type] === "function") { messageProcess[msg.type](msg.url); } });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const messageProcess = {
    // 删除html index
    1: function (url) {
        util.delCache(url);
    }
};
 
let util = {
    delCache: function (url) {
        caches.open(CACHE_NAME).then(cache => {
            console.log("delete cache " + url);
            cache.delete(url, {ignoreVary: true});
        });
    }
};
 
this.addEventListener("message", function(event) {
    let msg = event.data;
    console.log(msg);
    if (typeof messageProcess[msg.type] === "function") {
        messageProcess[msg.type](msg.url);
    }
});

根据不同的消息类型调不同的回调函数,如果是1的话就是删除cache。用户发表完评论后会触发刷新页面,刷新的时候缓存已经被删了就会重新去请求了。

这样就解决了实时更新的问题。

2. Installing

在installing状态中,SW 脚本中的 install 事件被执行。在能够控制客户端之前,install 事件让我们有机会缓存我们需要的所有内容。

比如,我们可以先缓存一张图片,那么当SW控制客户端之后,客户点击该链接的图片,我们就可以用SW捕获请求,直接返回该图片的缓存。

若事件中有 event.waitUntil() 方法,则 installing 事件会一直等到该方法中的 Promise 完成之后才会成功;若 Promise 被拒,则安装失败,Service Worker 直接进入废弃(redundant)状态。

推送通知

渐进式网页应用可选择实现各种设备特定的硬件功能,例如推送通知。软件发布商和开发人员可以完全控制如何实现这个功能,从而为通知新内容提供创新的解决方案。

对于电子商务网站,这可能意味着一个全新的销售入口渠道,因为直接显示在手机上的推送通知的读取次数要远远超过电子邮件形式的新闻信札以及社交媒体上的状态更新等。

此外,安装了渐进式网页应用的用户还可以在其主屏幕上看到图标,这会在用户每次使用手机时提醒他品牌名称和产品。这不仅仅是另一种销售策略,还可带来宝贵的品牌意识。但是如果用户安装许多应用程序和渐进式网页应用,通过推送通知发布最新产品、博客帖子(blog posts)、文章或其他相关信息, 可能会导致用户的通知区域杂乱无章。

感谢2016年7月的Google研究。

在所有下载渐进式网页应用的用户中,将近60%都授予渐进式网页应用发布推送通知的权限, 不过还有36.3%的用户没有点开推送通知,或者由于渐进式网页应用的个人设置没有收到推送通知。

将此数字与有多少网站访问者从主页上下载渐进式网页应用的统计数据结合起来,我们可以估计大约6-7%的网站现有流量能够转换为接受推送通知的渐进式网页应用用户。

2. navigator.serviceWorker

返回一个 ServiceWorkerContainer对象,该对象允许我们对SW进行注册、删除、更新和通信。

上面的代码中首先判断navigator是否有serviceWorker属性(存在的话表示浏览器支持SW),如果存在,那么通过navigator.serviceWorker.register()(也就是ServiceWorkerContainer.register())来注册一个新的SW,.register()接受一个 路径 作为第一个参数。

ServiceWorkerContainer.register()返回一个Promise,所以可以用.then().catch()来进行后续处理。

Service Worker的兼容性

一、 生命周期

个人觉得先理解一下它的生命周期很重要!之前查资料的时候,很多文章一上来就监听install事件、waiting事件、activate事件……反正我是一脸懵逼。

图片 12

Service Worker的生命周期

五、 fetch事件

现在我们的SW已经激活了,那么可以开始捕获网络请求,来提高网站的性能了。

当网页发出请求的时候,会触发fetch事件。

Service Workers可以监听该事件,'拦截' 请求,并决定返回内容 ———— 是返回缓存的数据,还是发送请求,返回服务器响应的数据。

下面的代码中,SW会检测缓存中是否有用户想要的内容,如果有,就返回缓存中的内容。否则再发送网络请求。

// sw.js

self.addEventListener('fetch', event => {
    const { request } = event; // 获取request
    const findResponsePromise = caches.open(CACHE_NAME)
    // 在match的时候,需要请求的url和header都一致才是相同的资源
    // caches.match(event.request, {ignoreVary: true}) 表示只要请求url相同就认为是同一个资源。
    .then(cache => cache.match(request)) // 查看cache对象中是否有匹配的项
    .then(response => {
        if (response) { // 如果response不为空,则返回response,否则发送网络请求
            return response;
        }

        return fetch(request);
    });
    // event.respondWith 是一个 FetchEvent 对象中的特殊方法,用于将请求的响应发送回浏览器。它接收一个对响应(或网络错误)resolve 后的 Promise 对象作为参数。
    event.respondWith(findResponsePromise);
});

箭头函数真的很适合用于Promise对象,省略了一堆的functionreturn关键字,看着舒服多了……

关于缓存策略
不同的应用场景需要使用不同的缓存策略。

比如,小红希望她的网站在在线的时候总是返回缓存中的内容,然后在后台更新缓存;在离线的时候,返回缓存的内容。

比如,小明希望他的网站可以在在线的时候返回最新的响应内容,离线的时候再返回缓存中的内容。
……
如果想要研究一下各种缓存策略,可以参考下面的资料,这里就不详述了,不然文章就成裹脚布了……
The Service Worker Cookbook
离线指南
Service Worker最佳实践

不过,既然标题是“做一个离线网页应用”,那我们就做一个最简单的缓存策略:如果缓存中保存着请求的内容,则返回缓存中的内容,否则,请求新内容,并缓存新内容。

self.addEventListener('fetch', function (event) {
    event.respondWith(
        caches.match(event.request)
        .then(response => {
            // Cache hit - return response
            if (response) {
                return response;
            }
            // 克隆请求。因为请求是一个“stream”,只能用一次。但我们需要用两次,一次用来缓存,一次给浏览器抓取内容,所以需要克隆
            var fetchRequest = event.request.clone();
            // 返回请求的内容
            return fetch(fetchRequest).then(
                response => {
                    // 检查是否为有效的响应。basic表示同源响应,也就是说,这意味着,对第三方资产的请求不会添加到缓存。
                    if (!response || response.status !== 200 || response.type !== 'basic') {
                        return response;
                    }
                    // 同request,response是一个“stream”,只能用一次,但我们需要用两次,一次用来缓存一个返回给浏览器,所以需要克隆。
                    var responseToCache = response.clone();
                    // 缓存新请求
                    caches.open(CACHE_NAME)
                        .then(cache => cache.put(event.request, responseToCache));
                    return response;
                }
            );
        })
    );
});

 
完成啦!我们简陋的离线应用!
打开页面,看一下缓存中有什么内容:

图片 13

offline1

然后点击“Vue”的链接:

图片 14

offline2

可以看到缓存中多了一张后缀为.png的图片。
SW缓存了我们的新请求!

打开chrome的开发者工具,点击offline,使标签页处于离线状态:

图片 15

offline3

然后,刷新页面。

图片 16

offline4

依然可以访问页面。

3. Installed / Waiting

如果安装成功,Service Worker 进入installed(waiting)状态。在此状态中,它是一个有效的但尚未激活的 worker。它尚未纳入 document 的控制,确切来说是在等待着从当前 worker 接手。

处于 Waiting 状态的 SW,在以下之一的情况下,会被触发 Activating 状态。

  • 当前已无激活状态的 worker
  • SW 脚本中的 self.skipWaiting() 方法被调用
  • 用户已关闭 SW作用域下的所有页面,从而释放了此前处于激活态的 worker
  • 超出指定时间,从而释放此前处于激活态的 worker

三、install 安装

我们已经注册好了SW,如果 sw.js 下载并且解析成功,我们的SW就进入安装阶段了,这时候会触发install事件。我们一般在install事件中缓存我们想要缓存的静态资源,供SW控制主页面之后使用:

// sw.js

var CACHE_NAME = 'my-site-cache-v1'; // cache对象的名字
var urlsToCache = [ // 想要缓存的文件的数组
  '/',
  '/styles/main.css',
  '/script/main.js'
];

// 如果所有文件都成功缓存,则将安装成功
self.addEventListener('install', function(event) {
  // 执行安装步骤
  // ExtendableEvent.waitUntil()方法延长了安装过程,直到其传回的Promise被resolve之后才会安装成功
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        // console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

2. caches

caches是一个CacheStorage对象,提供一个可被访问的命名Cache对象的目录,维护字符串名称到相应Cache对象的映射。

我们可以通过该对象打开某一个特定的Cache对象,或者查看该列表中是否有名字为“xxx”的Cache对象,也可以删除某一个Cache对象。

6. Redundant 废弃

Service Worker 可能以下之一的原因而被废弃(redundant)——

  • installing 事件失败
  • activating 事件失败
  • 新的 Service Worker 替换其成为激活态 worker

 
我们已经理解了SW的生命周期了,那么现在就开始来做一个离线应用。

我们只实现最简单的功能:用户每发送一个http请求,我们就用SW捕获这个请求,然后在缓存里找是否缓存了这个请求对应的响应内容,如果找到了,就把缓存中的内容返回给主页面,否则再发送请求给服务器。

3. SW的作用域

如果没有指定该SW的作用域,那么它的默认作用域就是其所在的目录。
比如,.register('/sw.js')中,sw.js在根目录中,所以作用域是整个项目的文件。

如果是这样:.register('/controlled/sw.js'),sw.js的作用域是/controlled。

我们可以手动为SW指定一个作用域:
.register('service-worker.js', { scope: './controlled' });

知识点:

  1. 使用限制
    Service Worker由于权限很高,只支持https协议或者localhost。
    个人认为Github Pages是一个很理想的练习场所。
  2. 储备知识
    Service Worker大量使用Promise,不了解的请移步:Javascript:Promise对象基础

兼容性

3. 为什么在load事件中进行注册

为什么需要在load事件启动呢?因为你要额外启动一个线程,启动之后你可能还会让它去加载资源,这些都是需要占用CPU和带宽的,我们应该保证页面能正常加载完,然后再启动我们的后台线程,不能与正常的页面加载产生竞争,这个在低端移动设备意义比较大。

知识点:

四、activate 激活

我们的SW已经安装成功了,它可以准备控制客户端并处理 push 和 sync 等功能事件了,这时,我们获得一个 activate 事件。

// sw.js

self.addEventListener("activate", function(event) {
    console.log("service worker is active");
});

如果SW安装成功并被激活,那么控制台会打印出"service worker is active"。

如果我们是在更新SW的情况下,此时应该还有一个旧的SW在工作,这时我们的新SW就不会被激活,而是进入了 "Waiting" 状态。

我们需要关闭此网站的所有标签页来关闭旧SW,使新的SW激活。或者手动激活。

那么activate事件可以用来干什么呢?假设我们现在换了一个新的SW,新SW需要缓存的静态资源和旧的不同,那么我们就需要清除旧缓存。

为什么呢?因为一个域能用的缓存空间是有限的,如果没有正确管理缓存数据,导致数据过大,浏览器会帮我们删除数据,那么可能会误删我们想要留在缓存中的数据。

这个以后会详细讲,现在只需要知道activate事件能用来清除旧缓存旧可以了。

本文由前端php发布,转载请注明来源:渐进式网页应用PWA,离线网页应用