实现一个 PWA 应用

PWA 的英文全称是 Progressive Web Apps,中文翻译过来就是渐进式 Web 应用。Google 在 2015 年开始推广这类无需下载的应用,运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序,为网页提供 App 般使用体验的一系列方案。

PWA 实际上仍是网页,只不过它添加了App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能。

PWA 和小程序有什么区别

PWA 的特点是:无需安装、更轻量、不占用大量空间,只需要一款支持 PWA 应用的浏览器,就可以轻松添加 PWA 应用,具备了跨平台使用的特性。

而微信小程序这类应用,必须在安装微信的前提下才可以使用微信小程序,并且有些小程序强制用户关联微信账号后才能使用。与其相比,PWA 应用展示了更开放的一面。

优势

  • 无需安装

  • 快捷方式添加到桌面上,点击主屏幕图标可以实现启动动画以及隐藏地址栏

  • 离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能

  • 消息推送

  • 内容可以通过搜索引擎发现

  • 使用 URL 分享

  • 在任何具有屏幕和浏览器的设备上可以正常使用

浏览器支持

PWA 所需的关键技术是 service worker 。 目前桌面和移动设备上的所有主流浏览器都支持 service worker。

service worker 浏览器支持情况

其他的 Web App ManifestPushNotificationsAdd to Home Screen 功能也得到了广泛的支持。 目前,Safari 对 Web App ManifestAdd to Home Screen 的支持有限,并且不支持 Web 推送通知。 但是,其他主流浏览器支持所有这些功能。

准备

安装 http-server

通过 npm 安装
npm install --global http-server
通过 Homebrew 安装
brew install http-server

开始

准备一个 WEB 应用

一个应用包括 HTML 页面,CSS 样式,图片,JAVASCRIPT 脚本和字体。

WEB 应用文件结构
添加 manifest 文件

manifest 是网页清单,位于 WEB 应用的根目录,通过 JSON 形式列举了网站的所有信息,允许应用能够添加到主屏幕,从设备主屏幕直接启动。

manifest.json 文件如下:

{
    "name": "Progressive Web App",
    "short_name": "PWA",
    "description": "Progressive Web App Demo",
    "icons": [
        {
            "src": "icons/icon-32.png",
            "sizes": "32x32",
            "type": "image/png"
        },
        // ...
        {
            "src": "icons/icon-512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "start_url": "index.html",
    "display": "fullscreen",
    "theme_color": "#B12A34",
    "background_color": "#B12A34"
}

一个 WEB 应用至少需要 name 和一个图标 (带有 src, size 和 type)。description, short_name, 和 start_url 最好要提供。

详细字段介绍:

  • name: 网站应用的全名。

  • short_name: 用户主屏幕上的应用名字。

  • description: 应用的描述。

  • icons: 主屏幕的图标。

  • start_url: 启动应用的网址。

  • display: 应用的显示方式;可以是全屏,独立,最小UI或者浏览器。

  • theme_color: 浏览器的地址栏等 UI 元素的颜色。

  • background_color: 背景色,用于安装程序时和显示启动画面时。

添加 Service Worker

Service Workers 实现了如何正确缓存网站资源并使其在用户设备离线时可用,还提供处理通知,在单独的线程上执行繁重的计算等。它运行在页面的 JavaScript 主线程独立的线程上,并且对 DOM 结构没有任何的访问权限。API 是非阻塞的,并且可以在不同的上下文之间发送和接收信息。

Service workers 可以控制网络请求,修改网络请求,返回缓存的自定义响应,或合成响应。

Service Workers 就像介于服务器和网页之间的拦截器,能够拦截进出的 HTTP 请求,从而完全控制你的网站。

特点
  • 在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和拦截作用域范围内所有页面的 HTTP 请求。

  • 网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost)。

  • 控制打开的作用域范围下所有的页面请求。

  • 单独的作用域范围,单独的运行环境和执行线程。

  • 不能操作页面 DOM,可以通过事件机制来处理。

  • 事件驱动型服务线程。

注册 Service Worker
<script>
  if (navigator.serviceWorker !== null) {
    navigator.serviceWorker.register('sw.js')
    .then(function(registration) {
      console.log('Registered events at scope: ', registration.scope);
    });
  }
</script>

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

service worker 生命周期
安装

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

创建缓存名字为 pwa-sample 的变量,将需要缓存的文件记录在数组上:

var cacheName = 'pwa-sample';
var appShellFiles = [
  'index.html',
  'app.js',
  'style.css',
  'favicon.ico',
  'img/bg.png',
  'icons/icon-32.png',
];

将要缓存的图片和上面的文件合并在一起:

var images = [];
for(var i=0; i<10; i++) {
  images.push(`img/image${i}.jpg`);
}
var contentToCache = appShellFiles.concat(images);

监听 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);
    })
  );
});

waitUntil 里面的代码执行完毕之后才会开始安装,并返回一个 promise。caches 是一个特殊的 CacheStorage 对象,它能在Service Worker 指定的范围内提供数据存储的能力。

处理动态资源
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;
        });
      });
    })
  );
});

网络请求时可以监听 fetch 事件,在 caches 中去 match 事件的 request ,如果 response 不为空的话就返回 response ,否则返回 fetch 请求,在请求得到响应后缓存响应到 caches 中,当然在 fetch 事件中我们也可以手动生成 response 返回给页面,最后将 response 返回。

FetchEvent.respondWith 方法将会接管响应控制,它会作为服务器和应用之间的代理服务,允许我们对每一个请求手动处理成我们需要的数据并返回给页面。

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

更新资源
self.addEventListener('activate', function(e) {
  e.waitUntil(
    Promise.all(
      caches.keys().then(cacheNames => {
        return cacheNames.map(name => {
          if (name !== cacheStorageKey) {
            return caches.delete(name)
          }
        })
      })
    ).then(() => {
      return self.clients.claim()
    })
  )
})

缓存的资源会跟随着版本的更新过期,当我们把版本号更新,Service Worker 会根据缓存的字符串名称清除旧缓存,并将我们所有的文件(包括新的文件)添加到一个新的缓存中。

这个时候新的 Service Worker 会在后台被安装,而旧的 Service Worker 仍然会正确的运行,直到没有任何页面使用到它为止,这时候新的 Service Worker 将会被激活,然后接管所有的页面。

在新安装的 SW 中通过调用 self.clients.claim( ) 取得页面的控制权,这样之后打开页面都会使用版本更新的缓存,旧的 SW 脚本不在控制着页面之后会被停止。

Activate

Activate 用法跟 install 相同,通常用来删除已经不需要的文件或者做一些清理工作。

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);
        }
      }));
    })
  );
});

这样能够确保只有需要的文件会保留在缓存中,毕竟浏览器的缓存空间是有限的,手动清理掉不需要的缓存是一个不错的主意。

通知推送

推送通知 是两个相互独立的功能,也可以配合使用。推送功能通过从服务端推送新的内容而不需要客户端发起请求,它是由应用的 Service Worker 来实现的。通知功能则可以通过 Service Worker 来向用户展示一些新的信息,或者提醒用户应用已经更新了某些功能。

通知

显示通知之前需要请求用户授权。

var button = document.getElementById("notifications");
button.addEventListener('click', function(e) {
    Notification.requestPermission().then(function(result) {
        if(result === 'granted') {
            randomNotification();
        }
    });
});

授权的结果有三种:

  • default:用户没有做出选择的时候,授权结果会返回defalut
  • granted:用户已授权
  • denied:用户拒绝授权

一旦用户选择授权,这个授权结果对通知 API 和推送 API 两者都有效。

创建通知
function randomNotification() {
    var randomItem = Math.floor(Math.random()*10);
    var title = `notification${randomItem}`;
    var content = `Created by test${randomItem}.`;
    var image = `img/img-${randomItem}.jpg`;
    var options = {
        body: content,
        icon: image
    }
    var notification = new Notification(title, options);
    setTimeout(randomNotification, 30000);
}

上述代码每隔三十秒会创建一个通知,直到用户手动关闭它为止。

推送

推送比通知更复杂,我们需要先从服务端订阅一个服务,之后服务端会推送数据到客户端应用。

用户订阅服务后才能接收到服务器推送的通知。

当用户订阅服务时,服务器会储存所有的接收到的信息以便在后续需要的时候能将信息推送出去。

navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
  return registration.pushManager.getSubscription()
  .then(async function(subscription) {
    if (subscription) {                                                      
        return;
    }
    return registration.pushManager.subscribe({  // 订阅新用户                          
      userVisibleOnly: true, // 发送给用户的所有通知对他们都是可见的
      applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) // 从服务端取得并转化的VAPID key
    })
    .then(function (subscription) {
      // 订阅部分
    })
  });
});

在这部分代码里,注册完成之后,我们使用 registration 对象来发起订阅,然后使用subscription对象来结束这整个流程。

self.addEventListener('push', function(e) { /* ... */ });

为了能够接收到推送的消息,需要在 Service Worker 文件里面监听 push 事件,数据接收后通过通知的方式立刻展现给用户。

如何添加 PWA 应用

将 WEB 应用在浏览器打开,点击分享图标,从弹出框点击添加到主屏幕,这时你会发现在主屏幕出现了一个新的图标,从此便可以通过此快捷方式打开 PWA 应用。

浏览器页面
分享弹框
添加到主屏幕
主屏幕页面
主屏幕打开 PWA 应用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容