Hello PWA

2016年 Google 在 I/O 大会上提出一个 Next Web Generation 的概念 —— PWA 横空出世,2017年始开始流行,目前虽未完全成熟,但越来越火爆,可以预见未来会广为流行。你还在等什么?

一、Hello PWA

PWA(Progressive Web App)[1], 渐进式 WEB 应用,是提升 Web App 的体验的一种方法,给用户原生应用的体验。PWA 可以通过 Service Worker, Manifest 等新技术让站点具备离线可用、添加到桌面、实时消息提醒等功能,从功能和体验上无限接近原生 App。

1.1. 背景

自1990年万维网之父-蒂莫西·约翰·“蒂姆”·伯纳·李爵士,创建了 HTTP、HTML 和 WorldWideWeb (全世界第一个网页浏览器)以来,Web 技术和影响力在以惊人的速度增长。HTML5,CSS3,Webpack,React,VUE,Babel,SPA 等技术的成熟与发展仿佛让 Web 进入了百家争鸣的春秋时期,Web应用能做的事情越来越多,大家对web的希望也越来越高。

但随着移动时代的到来,web 应用因为不能离线访问,没有快捷入口和页面频繁卡顿等开始失宠。除了原生应用因离线能力,瞬时加载和可靠性强等优点爆炸性崛起外,Hybrid ,React Native 等 APP 开发模式似乎也有点如日中天的“赶脚”。作为一名 Web 前端开发工程师已经瑟瑟发抖,你呢? 莫慌,老大哥 Google 的工程师们早就“抖”完了,并在2015年提出2016年推出 PWA ,号称 PWA 将成为 Web 颠覆者的契机。

从上图我们可以看出除了原生功能体验、渲染性能,支持设备底层访问,网络要求等四个方面外, Web App 对比 Native、Hybrid、React Native,还是占据一定优势的。更让人兴奋的是 PWA 一定程度上解决了 Web App 的“劣势”(图中黄色背景部分),让 Web APP 在保留原有优势的基础上渐进式接近原生 App。

1.2. 主要特点&优势

可靠 – 即使在不稳定的网络环境下,也能瞬间加载并展现 快速 – 快速响应,并且有平滑的动画响应用户的操作 粘性 – 像设备上的原生应用,具有沉浸式的用户体验,用户可以添加到桌面

以上列举的是PWA的三个最主要的特点,要想了解所有特点,可到 PWA官网[2] 查看。

1.3. 主要技术

PWA并不是描述一个技术,而是一个技术的合集,包含以下几个主要技术:

1. Service Worker(详见本文第二节);

2.  App Manifest(详见本文第三节);

3. Push API:允许 Web 应用拥有接收服务器并推送消息的能力( Web App 内部的消息推送)。目前已得到安卓和 PC 上新版本主流浏览器的支持,ios平台不兼容;

4、Notifications API:允许 Web 应用向用户显示系统通知。目前已兼容大部分 PC 主流浏览器,尚不兼容移动端浏览器。

5、Background Sync:可延迟发送用户行为,直到用户网络连接稳定。目前几乎不兼容移动端浏览器,PC 上 Firefox、 Chrome、 Safari、 Edge 等浏览器均已兼容。可解决两个常见问题:
– 普通的页面发起的请求会随着浏览器进程的结束/或者 Tab 页面的关闭而终止;
– 无网环境下,没有一种机制能“维持”住该请求,以待有网情况下再进行请求。

二、Service Worker

2.1. 什么是 Service Worker?

将你的网络请求想象成飞机起飞,Service Worker 是路由请求的空中交通管制员。它可以通过网络加载,或甚至通过缓存加载。

空中交通管制员可以延迟甚至改变飞机的降落的机场,Service Worker 的行为方式也是如此,它可以重定向你的请求,甚至彻底停止。如上图所示,Service Worker 在浏览器和后端服务之间起到了“管制员”的作用,它可以让你全权控制网站发起的每一个请求,这为许多不同的使用场景开辟了可能性,离线访问只是其中一种。

2.2. 功能特性

关于 Service Worker 的功能特性,以下几点看似无聊,其实很重要,不妨开发过程遇到问题再回头看看。

要求 HTTPS 环境,开发过程中,一般浏览器也允许 host 为 localhost 或者 127.0.0.1; 运行在它自己的全局脚本上下文中; 不绑定到具体的网页,无法修改网页中的元素,因为它无法访问 DOM; 一旦被 install,就永远存在,除非被手动 unregister; 异步实现,内部大都是通过 Promise 实现,依赖 HTML5 fetch API[3]; Service Worker 的缓存机制是依赖 Cache API[4] 实现的;

2.3. 生命周期

Service Worker 包含以下几个生命周期:

正在安装(installing):发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存, install 事件回调中有两个方法: event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。 self.skipWaiting():执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。 已安装(installed):安装完成,等待其他的 Service Worker 线程被关闭。 正在激活(activating):处于 activating 状态期间,Service Worker 脚本中的 activate 事件被执行。我们通常在 activate 事件中,清理 cache 中的文件。 activate 回调中有以下两个方法: event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。 self.clients.claim():在 activate 事件回调中执行该方法表示取得页面的控制权, 这样之后打开页面都会使用版本更新的缓存。旧的 Service Worker 脚本不再控制着页面,之后会被停止。 已激活(activated):在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会)。并可以处理功能性的事件 fetch (请求)、 sync (后台同步)、 push (推送)等。 废弃状态(Redundant):这个状态表示一个 Service Worker 的生命周期结束。进入废弃 ( redundant ) 状态的原因可能为这几种: 安装 ( install ) 失败 激活 ( activating ) 失败 新版本的 Service Worker 替换了它并成为激活状态

2.4. 主要事件

Service Worker 是基于事件的,安装、激活、缓存、通信等操作都是需要在特定事件下操作,包含以下几个主要事件。

2.5. 使用 Service Worker 缓存

上面一小节,我们了解了常用的几个事件,本节让我们一起利用这些事件缓存资源。(看代码的时候注意注释哦)

(1) 注册

首先要注册 Service Worker, 我们需要注册 Service Worker 来启动安装。 sw.js 文件推荐在 HTML 当中引入:

if ('serviceWorker' in navigator) {
    //如果浏览器支持 Service Worker API,在页面 onload 的时候注册位于 /sw.js 的 Service Worker。
    //启动一个线程很耗时,建议放到onload事件中 
    window.addEventListener('load', function () {
        navigator.serviceWorker.register('/sw.js', {scope: '/'})//scope 指定网域目录上所有事项的 fetch 事件
            .then(function (registration) { // 注册成功
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            })
            .catch(function (err) {  // 注册失败
                console.log('ServiceWorker registration failed: ', err);
            });
    });
}

(2) 通过 install 事件做静态缓存

接着我们往 sw.js 文件中添加逻辑,我们先尝试用 install 事件来做静态缓存。

为了帮助大家更好的理解下文的代码,请先熟悉上文生命周期中的几个方法和以下几个 Service Worker 的全局变量:

var cacheName = 'helloPwa1'; //缓存的名称                
self.addEventListener('install', event => { //安装
  //确保 Service Worker 不会在 waitUntil() 里代码执行完毕之前安装完成。
  event.waitUntil( 
    //用我们指定的缓存名称来打开缓存     
    caches.open(cacheName)      
    //把 JavaScript 和 图片文件添加到缓存中       
    .then(cache => cache.addAll([            
        './js/script.js',
        './images/hello.png',
        './images/logo.webp',
    ]))
  );
});
/*敲黑板*/
//如果任何文件下载失败了,那么安装过程也会随之失败。如果文件列表很长会增加缓存失败的几率导致 Servicer Worker 无法安装。

(3) 通过 fetch 事件使用缓存和处理动态缓存

Service Worker 会拦截浏览器所有请求,并查询当前 cache,如果存在 cache 则直接返回,若不存在,则通过 fetch 方法向服务端发起请求,并返回请求结果给浏览器。

//监听fetch事件来拦截请求
self.addEventListener('fetch', function(event) { 
  event.respondWith(
    //当前请求是否匹配缓存中存在的任何内容
    caches.match(event.request) 
    .then(function(response) {
      if (response) {
        //如果匹配的话,就此返回缓存并不再继续执行 
        return response;
      }
        //克隆了请求。请求是一个流,只能消耗一次。(很重要一步)
      var requestToCache = event.request.clone();
      //尝试按预期一样发起原始的 HTTP 请求
      return fetch(requestToCache).then( 
        function(response) {
          //如果由于任何原因请求失败或者服务器响应了错误代码,则立即返回错误信息
          if (!response || response.status !== 200) {
            return response;
          }
          //再一次克隆响应,因为我们需要将其添加到缓存中,而且它还将用于最终返回响应
          var responseToCache = response.clone();
          //打开名称为 “helloWorld” 的缓存
          caches.open(cacheName) 
          .then(function(cache) {
            // 将响应添加到缓存中
            cache.put(requestToCache, responseToCache); 
          });
          return response;
        }
      );
    })
  );
});

(4) 通过 active 事件更新缓存

当我们将资源缓存后,除非注销 sw.js 或手动清除缓存,否则新的静态资源无法缓存。这个时候在我们可以在 activate 事件中检查 cacheName 是否变化,如果变化则表示有了新的缓存资源,则将原有缓存删除。所以在 sw.js中加入以下代码后,当需要更新缓存时,我们仅仅需要修改 cacheName 就可以了。

self.addEventListener('activate', function (e) {
    var cachePromise = caches.keys().then(function (keys) {
        return Promise.all(keys.map(function (key) {
            if (key !== cacheName) {
                return caches.delete(key);
            }
        }));
    })
    e.waitUntil(cachePromise);
    return self.clients.claim();
});

除了通过 active 事件更新缓存,我们还可以在注册 Service Worker 的时候借助 Registration.update() 更新。

var version = '1.0.1';//每次更新改这个版本号即可
navigator.serviceWorker.register('/sw.js').then(function (registration) {
    if (localStorage.getItem('sw_version') !== version) {
        registration.update().then(function () {
            localStorage.setItem('sw_version', version)
        });
    }
});

2.6. 兼容性

2.7. 小结

咳咳咳,关于 Service Worker,本文先聊这么多,后续会有专文跟大家讨论,请持续我们的JDC网站或全栈探索微信公众号。

三、Manifest

3.1. Manifest是什么?

manifest 的目的是将Web应用程序安装到设备的主屏幕,为用户提供更快的访问和更丰富的体验。

3.2. 安装 Web App 到主屏幕条件

站点支持 HTTPS 访问; 站点部署 manifest.json; 站点注册 Service Worker;

3.3. manifest.json

PWA 添加至桌面的功能实现依赖于 manifest.json 文件,一个基本的 manifest.json 文件应包含如下信息:

{
    "name": "莎士比亚", //web app 的名称
    "short_name": "莎士比亚", //简称,没有足够空间的时候显示
    "description": "人工智能撰稿与设计平台", //简介
    "icons": [{ //图标
        "src": "./icon8.png",
        "sizes": "150x150",
        "type": "image/png"
    }, {
        "src": "./icon1.png",
        "sizes": "250x250",
        "type": "image/png"
    }],
    "background_color": "#fff", //背景颜色
    "theme_color": "#000", //主题色
    "start_url": "../index.html", //Web App 启动时的html文件,地址路径相对于mainfest.json文件
    "display": "standalone", //显示类型: 包含 fullscreen,standalone,minimal-ui,browser
    "orientation": "portrait" //显示方向,包含横屏,竖屏,自适应等
}

3.4. 引用manifest.json

manifest.json 配置的 start_url 对应的 html 文件的 head 标签中按如下方式引用即可:

<link rel="manifest" href="./manifest.json">

3.5. 使用 manifest 实现 Web APP 的启动页

启动页示例

通过配置 manifest.json 的下列属性,可以很容易的实现上图的 Web App 启动页:

设置图像和标题:标题则直接取自 name。浏览器会从 icons 中选择最接近 128dp 的图片作为启动画面图像。 设置启动背景颜色:支持 #ffffff , #fff , white , rgb(255,255,255) 格式,其他不支持,如 rgba 。 设置启动显示类型:仅当显示类型 display 设置为 standalone 或 fullscreen 时,PWA 启动的时候才会显示启动画面。

3.6. 小结

看到这里,你已经可以动手去把你的 Web APP 添加到桌面啦,由于兼容性问题,建议在 Android 或 PC 上使用 Chrome 浏览器体验。(PC 上需要配置谷歌浏览器,打开“chrome://flags/”中允许Desktop PWAs即可)

写在最后的话

本文旨在和大家一起学习 PWA 的入门知识,分别介绍了 PWA 的现状,特点以及其核心技术 Service Worker 和 Manifest。后续还会有文章和大家一起深入学习 PWA。欢迎各位提出问题和建议,共同成长和进步。 让热爱 Web 的我们一起书写 Web 的未来!

扩展阅读

[1] https://developers.google.com/web/progressive-web-apps/

[2] https://developers.google.cn/web/progressive-web-apps/checklist

[3] https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API

[4] https://developer.mozilla.org/zh-CN/docs/Web/API/Cache

文章来源于 全栈探索 微信公众号,扫描下面二维码关注:

文章来源:

Author:甄玉磊
link:https://jdc.jd.com/archives/212666