专栏首页黯羽轻扬Progressive Web Apps

Progressive Web Apps

一.What?

A new way to deliver amazing user experiences on the web.

一种提升Web用户体验的方式。除了Web天生的(便捷)体验外,还有3个特点:Reliable, Fast, Engaging

  • 可靠:在不确定的网络环境下,也能立即加载,而不会(因为断网而)瞬间回到远古时代 可靠指的是离线缓存,断网状态走缓存,保证离线场景仍然可用,service worker配合cache API建立缓存-代理机制
  • 快速:迅速以丝滑的动画作为交互反馈,而不存在掉帧卡顿的滚动 快速,只是强调交互反馈“感觉快”,与推崇的Material Design有关,并没有真正的速度优势(至少首屏没有) 另外,得益于缓存-代理机制,再次访问时走本地缓存会相当快
  • 类native:像设备原生App一样,具有沉浸式的用户体验(即全屏) 除了全屏外,还有主屏图标(让Web App在主屏幕有一席之地)和系统通知(“拉活”的能力),通过Web App Manifest配置来实现,依赖用户环境支持

P.S.Engaging这个抽象形容词真不好翻译,这里暂且取其实际意义,类native

所以,表面上看,PWA的亮点分2部分:

  • (离线)缓存-代理机制
  • 全屏,主屏图标和系统通知等类native特性

缓存机制在Web App/SPA里一点不新鲜,抽离出数据层之后,缓存顺手就做了。但侧重点不同,PWA的缓存机制偏向于静态资源缓存,而Web App/SPA的缓存层多用来做动态内容缓存(上次的内容没过期的话,不再重新获取动态部分,而是直接做客户端渲染)

至于全屏,主屏图标以及系统通知等类native特性,算是渐进增强中的增强,在支持的用户环境是可用的(一些浏览器提供了支持,但更广泛的WebView环境在不久的将来可能还是不行)。但这表明Web正在以渐进增强的方式走出PC时代,向着移动化发展

二.试玩

依赖环境

  • HTTPS

要求服务源必须是安全的,所以需要HTTPS环境。除了出于Web信息安全的考虑,想要推进HTTPS普及也是一个重要原因,HTTPS作为Web技术发展的必要基础设施,对于拍照,录音,push API等新特性,都需要获得用户许可,而HTTPS是权限工作流的关键部分,必不可少

P.S.在permission.site能够体验到HTTPS与HTTP环境在获取用户授权方面的差异

类native增强

通过引入Web App Manifest配置文件来实现类native增强,在支持PWA的浏览器生效(在不支持的环境最坏结果也就是多请求一个JSON文件):

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

注意,有个比较相似的东西,叫Application Cache(HTML5特性,已过时),其manifest引入方式不同:

<html manifest="example.appcache">
 ...
</html>

因为二者引入方式不同,所以Web App Manifest与Application Cache是不相干的,没有历史包袱的后顾之忧

P.S.Application Cache对SPA支持较好,对多页应用则不适用,但存在很多问题,这里不多做介绍

主屏图标

Web App Manifest内容示例如下:

{
 "short_name": "主屏显示的应用名称",
 "name": "安装banner显示的应用名称",
 "icons": [
   {
     "src": "launcher-icon-1x.png",
     "type": "image/png",
     "sizes": "48x48",
     "density": "1.0"
   },
   {
     "src": "launcher-icon-2x.png",
     "type": "image/png",
     "sizes": "96x96",
     "density": "1.0"
   },
   {
     "src": "launcher-icon-4x.png",
     "type": "image/png",
     "sizes": "192x192",
     "density": "1.0"
   }
 ],
 "start_url": "index.html?launcher=true"
}

P.S.安装banner是指一个类似于获取权限的弹出面板,用户可以选择添加至主屏幕或取消,满足一定条件的话,Chrome会自动弹出安装banner,具体见Web App Install Banners

这样理想情况下我们就拥有了主屏图标,支持Web App Manifest的环境会选用最合适的(最接近48dp的)图标

注意:index.html里的内容应该是首屏渲染需要的最小化内容,为了达到首屏立即加载的效果,可以把带loading和默认占位图的页面框架作为App Shell展示出来。另外,为了达到秒开可用的首屏性能,Web App首屏性能优化其它常规手段在PWA也是推荐使用的,比如数据直出。如开篇所说,PWA并没有天生的(首屏)性能优势,Web App适用的常规优化手段仍然是必要的

闪屏(Splash)

从主屏图标进入,可定制的启动过程显示内容包括:标题,背景色和图像。新配置项如下:

// 背景色
"background_color": "#2196F3",
// 主题色,包括工具栏
"theme_color": "#2196F3",

图像从icons中选取最接近128dp的图像作为闪屏,支持动图

另外,还可以指定显示模式和页面方向:

// 全屏(隐藏浏览器的UI)
"display": "standalone",
// 显示浏览器外壳,像打开书签一样
"display": "browser",
// 横屏
"orientation": "landscape"

P.S.关于闪屏的示例及更多信息请查看Adding a Splash Screen for Installed Web Apps in Chrome 47

特别注意:如果manifest.json文件有更新,这些改动不会自动生效,除非用户重新添加应用到主屏

系统通知

与Web App Manifest无关,依赖Push API。简单示例如下:

// service-worker.js
self.addEventListener("push", function (event) {
 event.waitUntil(
   self.self.registration.showNotification("发布新文章啦", {
     body: "有新文章发布啦,点击查看。"
   })
 );
});

这里不多做介绍(目前(2017/12/15)几乎可以认为这个特性不存在),因为规范定义了API,但没规定统一个push协议,所以各家浏览器的push机制不同,比如Chrome的GCM在我们这片天空下就不可用

关于Push API的更多信息,请查看【Service Worker】消息推送功能“全军覆没”

缓存-代理

缓存分为几部分:

  • 首屏静态资源缓存(预缓存)
  • 已访问资源缓存(运行时缓存)
  • 动态内容缓存(运行时缓存)

缓存是纯数据操作(包括持久化),而service worker能够在后台运行,尤其适合处理这种与页面及交互无关的事情,所以service worker与Cache API,Push API成了搭档。但service worker自身也应该看做“增强”项,在不支持service worker的环境应该跳过缓存机制保证基本的页面体验,简单的特征检测方案如下:

if ('serviceWorker' in navigator) {
 navigator.serviceWorker
          .register('./service-worker.js')
          .then(function() { console.log('Service Worker Registered'); });
}

service worker在install事件处理器完成包括App Shell在内的首屏静态资源缓存:

// service-worker.js
var cacheName = 'weatherPWA-step-6-1';
var filesToCache = [
 // 入口URL
 '/',
 '/index.html',
 '/scripts/app.js',
 '/styles/inline.css',
 // App Shell需要的资源
 '/images/ic_add_white_24px.svg',
 '/images/ic_refresh_white_24px.svg',
 // 内容展示可能用到的资源
 '/images/clear.png',
 '/images/cloudy-scattered-showers.png',
 '/images/cloudy.png',
 '/images/fog.png',
 '/images/partly-cloudy.png',
 '/images/rain.png',
 '/images/scattered-showers.png',
 '/images/sleet.png',
 '/images/snow.png',
 '/images/thunderstorm.png',
 '/images/wind.png'
];self.addEventListener('install', function(e) {
 console.log('[ServiceWorker] Install');
 e.waitUntil(
   caches.open(cacheName).then(function(cache) {
     console.log('[ServiceWorker] Caching app shell');
     //! 只要有一个失败就不接着做下一个了
     return cache.addAll(filesToCache);
   })
 );
});

当然,还需要对缓存做基本的版本控制:

// service-worker.js
self.addEventListener('activate', function(e) {
 console.log('[ServiceWorker] Activate');
 e.waitUntil(
   caches.keys().then(function(keyList) {
     return Promise.all(keyList.map(function(key) {
       // 以为cacheName为cache key,如果存在旧的缓存,删除掉
       if (key !== cacheName) {
         console.log('[ServiceWorker] Removing old cache', key);
         return caches.delete(key);
       }
     }));
   })
 );
 // 要求立即激活service worker,避免边界case
 return self.clients.claim();
});

P.S.边界case指的是某些情况下service worker无法立刻恢复激活态,导致不走缓存。为了屏蔽这些边界case,推荐使用GoogleChromeLabs/sw-precache帮助处理缓存控制问题(包括过期,更新策略等等)

缓存有了,接下来实现代理部分,拦截请求,并把缓存内容作为响应:

// service-worker.js
// 拦截请求
self.addEventListener('fetch', function(event) {
 console.log('[ServiceWorker] Fetch', e.request.url);
 // 自定义响应内容
 e.respondWith(
   // 查找缓存,没有才请求
   caches.match(e.request).then(function(response) {
     return response || fetch(e.request);
   })
 );
});

到这里基本的缓存-代理机制就准备好了,我们做了这些事情:

  1. 按资源列表预先缓存静态资源
  2. 拦截请求
  3. 把缓存内容作为响应给过去

有3个注意事项

  • 浏览器缓存可能会影响缓存更新,所以install事件处理器中的请求不会走缓存,而是直接进入网络
  • 注销service worker不会清掉缓存,cache key不变的话,之后还会拿到旧的缓存内容
  • 默认新注册的service worker在页面重新载入之后才会生效,除非做特殊处理

另外,我们的简版实现还存在一些问题,例如:

  • 缓存版本控制依赖一个静态的cache key,每次更新service-worker.js都要修改这个key
  • 一旦cache key有变化,会抹掉所有缓存,重新请求一遍,对于静态资源有些浪费
  • 缺少运行时缓存,资源列表不够灵活,期望更强大的边访问边缓存

第1个问题没什么太好的办法,第2个问题可以通过细分资源类型来缓解,例如:

// Shorthand identifier mapped to specific versioned cache.
var CURRENT_CACHES = {
 font: 'font-cache-v' + FONT_CACHE_VERSION,
 css: 'css-cache-v' + CSS_CACHE_VERSION,
 img: 'img-cache-v' + IMG_CACHE_VERSION
};

通过更细粒度的版本控制,能在一定程度上降低强制更新缓存的成本,当然,缓存层下面还有HTTP Cache兜底,缓存更新成本不是非常关键

至于运行时缓存,实际上只需要再做最后一小步就好了:

  1. 没命中缓存的话,请求资源*并缓存*

具体如下:

// 查找缓存,没有才请求
caches.match(e.request).then(function(response) {
 return response || fetch(e.request).then(function(res) {
   return caches.open(dataCacheName).then(function(cache) {
     // 并缓存起来
     cache.put(e.request.url, res.clone());
     return res;
   )
 })
})

另外,还可以根据资源类型及场景要求,针对性的选用合适的缓存策略,例如:

// service-worker.js
self.addEventListener('fetch', function(e) {
 console.log('[Service Worker] Fetch', e.request.url);
 var dataUrl = 'https://cache.domain.com/fresh/';
 // 策略1:有实时性要求的资源,请求优先,fetch then cache
 if (e.request.url.indexOf(dataUrl) > -1) {
   e.respondWith(
     caches.open(dataCacheName).then(function(cache) {
       return fetch(e.request).then(function(response){
         cache.put(e.request.url, response.clone());
         return response;
       });
     })
   );
 } else {
   // 策略2:一般资源,缓存优先,cache falling back to fetch
 }
});

P.S.更多缓存策略,见参考资料部分

三.Demo

官方Demo:Weather PWA,可能无法正常访问

搬运Demo(把官方Demo挪到github pages):https://ayqy.github.io/pwa/demo/weather-pwa/index.html

P.S.github pages非常适合用作试验田,稳定可靠的HTTPS,发布内容没有任何限制可以随便折腾,以后的博客Demo都会逐步迁移过去(之前一直放在自己的FTP,那可真蠢..)

weather-pwa

不太乐观的消息:事实上,故意精心准备了用户环境(官方正版Chrome + 官方Demo),在小米4上没有自动弹出安装banner(可能是操作姿势等条件不满足,见上文),手动点击“添加至主屏幕”,toast添加成功,但主屏幕上啥也没有……这就是提不起兴趣手写Demo试玩的原因(当然,主要原因是懒;))

四.案例

  • 阿里巴巴国际站
  • AliExpress
  • 饿了么:奇怪,为什么没有感受到Cache的作用呢

注意,隐身模式可能会导致阿里巴巴国际站的service worker抛如下错误:

Uncaught (in promise) DOMException: Quota exceeded.

正常环境可正常体验

P.S.更多案例,请查看Case Studies | Web | Google | Developers

五.应用场景

简言之,PWA算是Web App的升级版,主要亮点是类native支持。以渐进增强的方式,不需要太高成本就能完成Web App到PWA的“升级”,让部分用户(支持PWA的环境)获得更快(缓存)更便捷(主屏图标)的类native体验(全屏)

那么具体应用场景分以下几种:

  • 缓存能带来明显收益的Web App
  • 期望具有离线能力,或类native体验,或者单纯只是想要个主屏图标的Web应用
  • 期望蹭个技术热点/协助推动其发展的Web应用或浏览器供应商

不管应用场景,话说回来,正如zxx某篇关于缓存(还是worker?)的文章所说,这么点儿成本就能让页面获得离线能力,真切看到缓存带来的收益,何乐而不为呢?

另外,Angular,React,Vue等主流框架都提供了PWA脚手架,具体请查看The Ultimate Guide to Progressive Web Applications

参考资料

  • The offline cookbook:缓存策略图解,好东西,待翻译,就着ServiceWorker Cookbook一起看
  • 如何看待 Progressive Web Apps 的发展前景?
  • Your First Progressive Web App:官方教程
  • A Beginner’s Guide To Progressive Web Apps:比较全面的入门指南
  • 改造你的网站,变身PWA:原文Retrofit Your Website as a Progressive Web App

本文分享自微信公众号 - 前端向后(backward-fe),作者:ayqy

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-12-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 听说,加缓存能提高性能?

    关注「前端向后」微信公众号,你将收获一系列「用心原创」的高质量技术文章,主题包括但不限于前端、Node.js以及服务端技术

    ayqy贾杰
  • HTTP缓存

    P.S.关于HTTP Header的更多信息,请查看4.2 Message Headers

    ayqy贾杰
  • 如何理解 Scalability?

    关注「前端向后」微信公众号,你将收获一系列「用心原创」的高质量技术文章,主题包括但不限于前端、Node.js以及服务端技术

    ayqy贾杰
  • Web缓存 - HTTP协议缓存

    Web缓存一般分为浏览器缓存、代理服务器缓存以及网关缓存,本文主要讲的是 浏览器缓存,其它两种缓存大家自行去了解下。

    laixiangran
  • 详解HTTP缓存

    HTTP缓存是一项重要且常见的web性能优化手段。当通过浏览器发送HTTP请求时,如果浏览器本地有所要请求的文档副本,那么浏览器可以直接从本地存储中读取该文档,...

    黄泽杰
  • 【DB笔试面试576】在Oracle中,简述Oracle中的游标。

    在介绍游标之前先介绍一下Oracle数据库中库缓存(Library Cache)的作用及其组成结构。库缓存是SGA中共享池(Shared Pool)中的一块内存...

    小麦苗DBA宝典
  • 缓存与数据库不一致,咋办?

    缓存与数据库的操作时序,不管是《Cache Aside Pattern》中的方案,还是《究竟先操作缓存,还是数据库?》中的方案,都会遇到缓存与数据库不一致的问题...

    架构师之路
  • Asp.net mvc 知多少(九)

    本系列主要翻译自《ASP.NET MVC Interview Questions and Answers 》- By Shailendra Chauhan,想...

    圣杰
  • 分布式缓存有哪些坑

    在toC的系统之中,解决高并发场景下低延迟的问题,缓存是很重要的解决手段。多级缓存,数据异构,数据预处理都是相关的高性能方法论,那么使用分布式缓存会有哪些坑呢?...

    春哥大魔王
  • 后端技能清单(草稿)

    昨天也顺手整理了一下我所需要的后端技能清单。不过,由于我离非常有经验的后端开发者有点距离,希望大家可以给点意见哈。 入门 HTML / CSS 编程语言:Ja...

    Phodal

扫码关注云+社区

领取腾讯云代金券