Update avaliable. Click RELOAD to update.
目录

Progressive Web App 汇总使用说明

1. PWA是什么

Progressive Web App,简称 pwa,中文名称为 渐进式网页应用。它的目的是利用web的技术给移动端的设备提供原生App的体验。

2. 有什么特性

3. 基本组成结构

pwa组成

断网或网络差,基于常驻后台的 Service Worker 实现数据离线存储与通知推送。

关于各个浏览器针对于 pwa 应用缓存的大小限制,请参看 附录A 不同浏览器对 PWA 应用缓存的限制

4. 编写原生的PWA应用

编写 PWA 应用前,应该已经有一个标准的响应式的 Web站点,可以正常在浏览器中打开。

另外建议在网站根路径中准备好 favicon.ico 的图标。

4.1 应用描述文件

manifest.json 文件是 Web 站点的描述文件,用来告诉浏览器 PWA应用 的基本信息,包括名称、图标、启动页、背景色等等。完整的 manifest.json 配置请参看 Web App Manifest properties

在工程的根路径中创建 manifest.json 文件,内容如下:

    Console.WriteLline("Hello");
{
    "name" : "欢迎来到 Vinny Blog",
    "short_name" : "Vinny Blog",
    "start_url" : ".",
    "display" : "standalone",
    "orientation" : "portrait",
    "background_color" : "#ccc",
    "theme_color" : "#5FAAE5",
    "description" : "这是一个简单测试的pwa应用",
    "icons" : [
        {
            "src" : "/assets/icons/icon-72x72.png",
            "type" : "image/png",
            "sizes" : "72x72"
        },
        // .... 省略 96,128,144,152,192,384,512像素icon
    ]
}

具体属性释义:

名称含义
name显示在”Add Home Screen”的横幅中
short_name桌面app的名称
display为standalone则隐藏浏览器UI,bowser则相反
orientation界面的初始方向,portrait竖屏,landscape横屏
background_colorapp启动画面的背景颜色
theme_colorapp状态栏背景色

如果觉得手写 manifest.json 文件过于繁琐,可以使用在线生成 Web App Manifest Generator 的方式。

4.2 首页文件注册 manifest.json

创建好的 manifest.json 文件,需要在 index.html 首页的 link 标签中进行定义,这样浏览器才会进行读取。

<head>
    ...

    <!-- 注册 manifest.json 文件 -->
    <link rel="manifest" href="/manifest.json">
    <!-- 应与 manifest.json 中的 theme_color属性 一致 -->
    <meta name="theme-color" content="#5FAAE5">

    <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="format-detection" content="telephone=no">
    <meta name="msapplication-tap-highlight" content="no">

    ...
</head>

这样的配置对于 Chrome 浏览器足够使用了,但对于 IOS 系统的 safari 浏览器仍然不行,IOS 系统在 11.3 版本之后才开始对 PWA 应用的支持,但仍然有不少问题,具体在如何适配请参看 附录B PWA在IOS中的问题与解决方法

4.3 服务工作文件

ServiceWorker 即工作线程,简称 sw,当访问站点时会以此域名将 sw 进行注册,一旦进行注册则会在浏览器中形成常驻的工作线程。值得注意的是 sw 不能直接操作 DOM 元素,它更像个代理程序,用来转发数据。

查看当前浏览器已经注册的服务工作线程 chrome://serviceworker-internals

在工程根路径中创建 service-worker.js 文件,文件的所在路径与作用域 scope 相关,注册在那个路径里,就只能对此路径有访问权限,在根路径中表示对整个站点有效。

下面是 service-worker.js 文件是一个简单的实现:

// 安装事件
self.addEventListener('install', event => {
    console.log('Service Worker Install')
});

// 激活事件
self.addEventListener('activate', event => {
    console.log('Service Worker Activate')
});

// 请求事件
self.addEventListener('fetch', event => {
    console.log(event.request.url);
});

4.4 脚本文件中注册 service-worker.js

脚本文件是站点的其他 js 文件,在此文件中进行 sw 的注册,才可以正常使用,注册如下:

// 判断浏览器是否支持 ServiceWorker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', e => {
    // 注册"serviceWorker"
    navigator.serviceWorker.register('/service-worker.js')
      .then(registration => {
        console.log('Service Worker registration success with scope: ', registration.scope);
      })
      .catch(err => {
        console.log('Service Worker registration failed: ', err);
      });
  });
}

4.5 发布到服务器中

按章节 3. 基本组成结构 中的图,至此准备工作结束,一个基本的 PWA 应用已经完成,现在可以将整个站点发布到 nginx、apache等服务器中。

这里值得一提的是,正确的 PWA 应用应该部署在 HTTPS 的环境下,即你需要安全证书,不支持自签名证书,关于如何获取免费的 HTTPS 证书请参看另一篇文章 使用Lets Encrypt为站点快速部署SSL证书

访问的截图如下:

横幅的弹出,是由浏览器进行控制,条件是”站点被访问至少两次,这两次访问至少间隔五分钟

4.6 实现基本的离线存储

目前我们的 service-worker.js 没有做任何事情,只是在生命周期中进行了输出,现在我们需要实现站点的 离线存储,修改后的脚本如下:

"use strict";

function onInstall(e) {
    e.waitUntil(
        caches.open(CACHE_NAME).then(function (e) {
            return e.addAll(URL_TO_CACHE).then(function () {
                console.log("WORKER: Install completed")
            })
        })
        .then(self.skipWaiting())
    );
}

function onActivate(e) {
    console.log("[Serviceworker]", "Activating!", e);

    // 这里的"keyList"就是"var CACHE_NAME = CACHE_VERSION + ":sw-cache::"的数组
    e.waitUntil(caches.keys().then(keyList => {
        // 这里的"key"就是"V1.0.1:sw-cache::",掉上面缓存名称的遍历
        return Promise.all(keyList.map(key => {
            if (key != CACHE_NAME) {
                console.log('[ServiceWorker] Removing old cache', key);
                return caches.delete(key);
            }
        }));
    }));
}


function onFetch(e) {
    // console.log('Fetch event for >>> ', e.request.url);

    // 调用respondWith()方法劫持GET请求并返回
    e.respondWith(
        caches.match(e.request).then(resp => {

            // 缓存命中直接返回
            if (resp) {
                return resp;
            }

            console.warn('Not in Cache... Making Network request for ', e.requet.url);

            var fetchRequest = e.request.clone();
            return fetch(fetchRequest).then(response => {
                // 检查是否收到无效的响应
                if (!response || response.status !== 200 || response.type !== 'basic') {
                    return response || caches.match("/offline.html");
                }

                var responseToCache = response.clone();
                caches.open(CACHE_NAME).then(cache => {
                    cache.put(e.request, responseToCache);
                });

                return response;
            })
            .catch (err => {
                // 没有检索到应该跳转到离线页面
                console.error('Failed to fetch', e.request.url)
                return caches.match('/offline.html');
            });
        })
    )
}

// ----------------------------------------------------------------------

var CACHE_VERSION = "V1.0.1";
var CACHE_NAME = CACHE_VERSION + ":sw-cache::";
var URL_TO_CACHE = [
    "/",
    "/manifest.json",
    "/offline.html",
    "/assets/css/amazeui.min.css",
    "/assets/css/app.css",
    "/assets/js/app.js",
    "/assets/js/jquery.min.js",
    "/assets/js/amazeui.min.js",
    "/assets/img/user01.png",
    "/assets/img/user02.png",
    "/assets/img/user03.png"
    // .....
];
// Service Worker 事件注册
self.addEventListener("install", onInstall), self.addEventListener("activate", onActivate), self.addEventListener("fetch", onFetch);

从上述脚本可知,我们对缓存定义了版本,版本主要实现缓存的更新,否则即使站点做了更新,用户在设备上访问的还是旧的内容。URL_TO_CACHE 定义了需要预缓存的基本结构,又叫 App Shell,在 install 中进行安装,在 fetch 中进行读取,命中则直接返回,否则重新请求。

更多完整的 ServiceWorker 生命周期介绍请参看 服务工作线程:简介

4.7 激活更新事件

当站点的内容更新发布到 HTTPS 服务器中时,用户在手机上访问的 PWA 应用并不会取更新的内容,还是继续从手机缓存中获取,此时就需要触发服务工作线程的更新,也就是 Active 激活动作,将老的缓存删除,重新从服务器中获取新的内容并缓存起来。

服务工作线程更新的条件

  1. 用户打开 PWA应用(可以使chrome中,也可以是桌面),浏览器会尝试从后台重新下载定义服务工作线程的脚本文件。若下载的服务工作线程文件与当前的服务器工作线程文件存在字节的差异,则视为新的服务工作线程。这意味着想要站点在用户设备上激活更新事件,每次发布都需要修改 service-worker.js 文件,在服务工作线程文件中修改版本号、回车、加个空格都视为字节的变化。

  2. 新服务工作线程会启动,且触发 install 事件

  3. 此时,旧服务工作线程仍控制着当前页面,新服务工作线程进入 waiting 状态。

    sw11

  4. 当关掉重新打开 PWA应用 时,就工作线程被终止,新服务线程取得控制权,同时触发 activate 事件,删除旧的缓存

    sw12总结激活更新

    更新 html 内容,修改 Service Worker,打开 PWA应用,由于服务工作线程已经改变,触发 install 事件,新的缓存被安装,关闭 PWA应用 再重新打开,接手,触发 activate 事件删除老缓存,页面刷新为新的html内容。

5. 对 PWA 应用进行审计

使用 Lighthouse 工具可以对 PWA 应用进行审计,以检查当前的 PWA 是否正常。此插件可以在 Chrome 浏览器进行安装,安装完成后,在浏览器中输入 PWA 应用的地址,点击 Lighthouse 插件进行审计工作。

这里我使用我的博客站点进行测试,最后生成的报告如下:

lighthouse

从上述的报告中可以看到,当前 https://wangjun.dev 的站点对于 PWA 应用支持的得分为 91,除了有一个问题,就是无法从 HTTP 请求转向到 HTTPS 请求。

附录A 不同浏览器对 PWA 应用缓存的限制

浏览器限制
Chrome可用空间 < 6%
FireFox可用空间 < 10%
Safari可用空间 < 50MB
IE10可用空间 < 250MB

附录B PWA在IOS中的问题与解决方法

IOS在11.3开始支持PWA应用,但兼容性仍然不能达到理想,现阶段智能调用有限的 API,具体主要表现为以下几个问题:

当然某些问题是可以解决的,如下:

桌面图标

IOS 的 Safari 浏览器暂时不支持读取 manifest.json 文件的 icon 配置信息,当添加到桌面时,只是页面的缩略图,对用户不是很友好,具体的改善步骤为,在 index.html 中的 head 标签内添加适配信息

<link rel="apple-touch-icon" href="/assets/icons/icon-96x96.png">
<link rel="apple-touch-icon" sizes="152x152" href="/assets/icons/icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/icon-180x180.png">
<link rel="apple-touch-icon" sizes="167x167" href="/assets/icons/icon-167x167.png">

启动页面

IOS 在启动 PWA应用时,启动页面默认是黑屏或者白屏,对于这个问题可以在 apple developer’s page 找到解决方案,通过加入 apple-touch-startup-image 标签解决,如下:

<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">

<link href="/apple_splash_2048.png" sizes="2048x2732" rel="apple-touch-startup-image" />
<link href="/apple_splash_1668.png" sizes="1668x2224" rel="apple-touch-startup-image" />
<link href="/apple_splash_1536.png" sizes="1536x2048" rel="apple-touch-startup-image" />
<link href="/apple_splash_1125.png" sizes="1125x2436" rel="apple-touch-startup-image" />
<link href="/apple_splash_1242.png" sizes="1242x2208" rel="apple-touch-startup-image" />
<link href="/apple_splash_750.png" sizes="750x1334" rel="apple-touch-startup-image" />
<link href="/apple_splash_640.png" sizes="640x1136" rel="apple-touch-startup-image" />

不支持弹出”Add to home screen”的横幅

在 Android 系统中,会弹出横幅鼓励用户将 PWA应用 添加到主屏幕中。但是在 IOS 系统中不会有次横幅弹出,想要添加到主屏,需要三个步骤。

对于这种方式增加到主屏肯定是不友好的,建议设计一个弹出层,以友好的方式提示用户将 PWA 应用添加到主屏中,这里需要注意的是,弹出层需要检查当前 PWA 应用是否是 window.navigator.standalone 的情况。

关于导航的问题

当在 IOS 下运行 PWA 应用时,若处于 standalone 模式时,由于苹果手机没有返回的按键,因此在设计应用界面时需要确保用户有返回控制键以及可以从任何位置返回到主页上。

这你可以通过显示后退按钮或者在IOS尚添加更多的菜单栏来实现。

附录C 实验中的PWA应用参考

关于 PWA应用 的完善版,可以参考本站点 vinny.cc 的实现,关键的文件如下:

https://vinny.cc/manifest.json

https://vinny.cc/assets/main.js

https://vinny.cc/service-worker.js

附录D 如何对PWA进行远程调试

这里的环境是使用Android系统的真机进行 PWA 应用的调试

  1. 手机的设置中打开 USB调试模式

  2. 使用USB线接入电脑或者在手机上安装 ADB 远程调试工具,如 网络adb

    若使用 远程adb 的方式,则如要使用如下命令建立连接:

    $ adb connect 192.168.1.198:5555
    $ adb devices
    
  3. 在电脑上打开 Chrome 浏览器并打开开发者工具,打开 Remote devices 应可以看见已连接上的Android设备

    pwd-debug-1

  4. 在手机中打开 PWA应用,在浏览器开发者工具中就可以看见并进行调试

    pwd-debug-2

另外如果 PWA应用 部署在本地,可以通过chrome浏览器的 Port forwarding 功能将本地 PWA应用 映射到手机上,这样在手机中输入”localhost:8887”即可在调试中出现。

pwa_debug-3

参考文章

版权所有,本作品采用知识共享署名-非商业性使用 3.0 未本地化版本许可协议进行许可。转载请注明出处:https://www.wangjun.dev//2018/05/pwa-app/