Progressive Web App, 简称 PWA,是「渐进式」提高 Web App 体验的一种新方法,能给用户相似原生应用的体验。css
「高可靠,高性能,优体验」是 PWA 惯用的形容词,他的另一个优势就是「渐进式」,开发者能够对照 PWA Checklist 逐步对本身站点进行 PWA 化升级。html
苹果前 CEO,Steve Jobs,2007 年 WWDC 上提出了为初代 iPhone 开发应用的概念,当时所介绍的,就是 Web App——能够从主屏直接启动的 Web 应用。java
惋惜当时这个理念太过超前,并无引起太多关注,反而是后来的原生 App 应用更符合当时的市场需求,互联网公司更愿意投入人力在原生 App 的开发上,而忽略了 Web。所以原生 App 的大量出现,占据了移动时代的主流地位,Web 彷佛就要被 App 所取代。git
随着 Web 技术的发展,时间来到 2014 年, W3C 公布了 Service Worker 的相关草案,其生产环境在 2015 年被 Chrome 支持。随后 PWA 加以完善,相关技术不断升级优化,在用户体验和用户保活两方面的可发掘价值愈来愈大。程序员
继移动站点喷井式发展之焰末,原生 App 的弊端愈加明显,对于它来说,最大的痛点即是其天生封闭的基因致使的内容没法被索引,相对的 Web 站点可索引的优点开始凸显,与此同时,PWA 遵循 W3C 标准开发的技术,彻底开放,可以快速地被各大浏览器厂商支持,市场支持度一晚上崛起。github
另一边,App 的推广并不顺利,据调查统计,移动设备用户 80% 的时间花费在了经常使用的 5 个应用上,16年近一半的美国用户平均每个月安装「0」个新 App,用户积极探索新 App 已经成为了过去式,拉新和保活的成本愈来愈高。web
原生 App 的发展遇到了天花板,推广也正向瓶颈一步步靠近,Web 看到了本身的机遇,PWA 以及支撑 PWA 的一系列关键技术应运而生。chrome
2018 年对于 PWA 来讲是里程碑的一年,万众瞩目的 Apple 终于在 iOS 11.3 里支持了 Web App Manifest,以及内置的 Safari 11.1 支持了 Service Worker。编程
与此同时,全球顶级浏览器厂商,Google、Microsoft、Apple 已经全数宣布支持 PWA 技术,这预示着,Web App 将会迎来全新的时代json
截至当下的 PWA 支持度
依据 Can I use 的统计(20190515)
Service Worker 以全数「登船」。信息来源于 jakearchibald.github.io/isservicewo…
PWA 有几个核心功能,分别是「离线,安装,推送」
弱网或离线的状况下依然能够「正常访问」甚至「秒开」,这种体验甚至超过了 app。主要的技术点就是 Service Worker。
SW 相似于咱们熟知的 Web Worker,Web Worker 能够脱离主线程,处理一些「脏累」活,干完后经过 postMessage 向主线程汇报工做结果。因此,SW 也是脱离主线程的存在,与 Web Worker 不一样的是,SW 具备持久化的能力。
SW 还具有有如下功能和特性:
基于以上咱们能够看到 SW 要让缓存作到极致优雅的伟大使命。
想要灵活的使用 SW 功能,就要充分了解他的生命周期,以及各阶段的状态。
如下是 MDN 给出的 SW 的详细生命周期图。
能够看到,SW 的生命周期包含这么几个状态 安装中
, 安装后
, 激活中
, 激活后
和废弃
安装( installing ):这个状态发生在 Service Worker 注册以后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存。 install 事件回调中有两个方法:
event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
self.skipWaiting():self 是当前 context 的 global 变量,执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。
安装后( installed ):Service Worker 已经完成了安装,而且等待其余的 Service Worker 线程被关闭。
激活( activating ):在这个状态下没有被其余的 Service Worker 控制的客户端,容许当前的 worker 完成安装,而且清除了其余的 worker 以及关联的旧缓存资源,等待新的 Service Worker 线程被激活。
activate 回调中有两个方法:
event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
self.clients.claim():在 activate 事件回调中执行该方法表示取得页面的控制权, 这样以后打开页面都会使用版本更新的缓存。旧的 Service Worker 脚本再也不控制页面,以后会被中止。
激活后( activated ):在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会)。并能够处理功能性的事件,fetch (请求)、sync (后台同步)和 push (推送)。
废弃状态 ( redundant ):这个状态表示一个 Service Worker 生命周期的结束。
这里特别说明一下,进入废弃状态的缘由可能为这几种:
安装 (install) 失败
激活 (activating) 失败
新版本的 Service Worker 替换了它并成功激活
MDN 也列出了 Service Worker 全部支持的事件:
install:Service Worker 安装成功后被触发的事件,在事件处理函数中能够添加须要缓存的文件
activate:当 Service Worker 安装完成后并进入激活状态,会触发 activate 事件。经过监听 activate 事件你能够作一些预处理,如对旧版本的更新、对无用缓存的清理等。
message:Service Worker 运行于独立 context 中,没法直接访问当前页面主线程的 DOM 等信息,可是经过 postMessage API,能够实现他们之间的消息传递,这样主线程就能够接受 Service Worker 的指令操做 DOM。
Service Worker 有几个重要的「功能性事件」,这些功能性的事件支撑和实现了 Service Worker 的特性。
fetch (请求):当浏览器在当前指定的 scope 下发起请求时,会触发 fetch 事件,并获得传有 response 参数的回调函数,回调中能够作各类代理和缓存操做。
push (推送):push 事件是为推送准备的。不过首先须要了解一下 Notification API 和 PUSH API。经过 PUSH API,当订阅了推送服务后,可使用推送方式唤醒 Service Worker 以响应来自系统消息传递服务的消息,即便用户已经关闭了页面。
sync (后台同步):sync 事件由 background sync (后台同步)发出。background sync 配合 Service Worker 推出的 API,用于为 Service Worker 提供一个能够实现注册和监听同步处理的方法。但它还不在 W3C Web API 标准中。在 Chrome 中这也只是一个实验性功能,须要访问 chrome://flags/#enable-experimental-web-platform-features ,开启该功能,而后重启生效。
有了以上 SW 的事件及 API,接下来就是实战部分了。
127.0.0.1
和 localhost
测试,但部署须在 https 协议下。巧妇难为无米之炊,以上两个先决条件是必需要知足的。
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js', {scope: '/'}).then(function(registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// 注册失败 :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
复制代码
其实,关键代码只有一行
navigator.serviceWorker.register('/sw.js', {scope: '/'})
复制代码
注意,此处有坑
Service Worker 的注册路径决定了其 scope 默认做用域,如 SW 注册文件的路径为 https://www.a.com/public/sw.js
时,对应默认 scope 是 /public/
,其做用范围以下
域名 | 是否生效 |
---|---|
www.a.com/ | 否 |
www.a.com/page/ | 否 |
www.a.com/public/ | 是 |
www.a.com/public/page… | 是 |
www.b.com/ | 否 |
www.b.com/public/ | 否 |
以上可看出,看成用域 scope 为 /public/
后,其做用范围只限于自己和子域,父域和兄弟域皆无效,跨域就更免谈了。
固然,咱们能够经过设置 scope 来限定本身的做用域,可是!请注意,『如下写法是错误的』。
navigator.serviceWorker.register('/public/sw.js', {scope: '/'}) // 错误写法
navigator.serviceWorker.register('/public/sw.js', {scope: '/page'}) // 错误写法
复制代码
以上写法均会报错
The path of the provided scope ('/') is not under the max scope allowed ('/public/'). Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.
因此,sw.js 文件最好放在根域名下
如
navigator.serviceWorker.register('/sw.js', {scope: '/page'})
复制代码
当 scope 不一样时,请求被监控状况也有不一样
代号 | 请求 |
---|---|
r1 | www.a.com/api |
r2 | www.a.com/page1/api |
r3 | www.a.com/page2/api |
r4 | www.a.com/static/img1.png |
r5 | www.b.com/api2 |
r6 | www.b.com/static/img2.png |
域名 | scope | 被监控请求 |
---|---|---|
www.a.com/ | / | r1-6 |
www.a.com/ | /page1 | 无 |
www.a.com/page1 | / | r1-6 |
www.a.com/page1 | /page1 | r1-6 |
www.a.com/page1 | /page2 | 无 |
因此,scope 与被监控请求的域并无什么关系,他只与站点域名有关
咱们能够经过打开 Chrome 的 DevTools
-> Application
-> Service Workers
查看 SW 的注册状况。
看到类如 Status: #xxxx activated and is running
,即说明注册并激活成功。
也能够经过打开 Chrome 的管理页 chrome://inspect/#service-workers
查看
在受控页面启动注册流程后,咱们来看看处理 install 事件的 Service Worker 脚本。
最基本的例子是,您须要为安装事件定义回调,并处理想要缓存的文件。
self.addEventListener('install', function(event) {
// Perform install steps
});
复制代码
在 install 回调的内部,咱们能够执行如下步骤(固然也能够啥也不干):
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js'
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
复制代码
此处,咱们以所需的缓存名称调用 caches.open()
,以后再调用 cache.addAll()
并传入文件数组。 这是一个 promise
链(caches.open()
和 cache.addAll()
)。 event.waitUntil()
方法带有 promise
参数并使用它来判断安装所花费的时间,以及安装是否成功。
若是全部文件都成功缓存,则将安装 Service Worker。 若有任意文件没法下载,则安装失败。此设计可保证 SW 启动的正确性,但过长的资源列表也增长了安装失败的概率,可根据项目状况自行定义,也可不定义。
在安装 Service Worker 且用户转至其余页面或刷新当前页面后,Service Worker 将开始接收 fetch 事件。下面提供了一个示例。
self.addEventListener('fetch', function(event) {
event.respondWith(
//匹配缓存
caches.match(event.request)
.then(function(response) {
//命中走观察
if (response) {
return response;
}
//未命中则透传向网络
return fetch(event.request);
}
)
);
});
复制代码
若是但愿连续缓存新请求,能够经过处理 fetch 请求的响应并将其添加到缓存来实现,以下所示。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
/** * 经过检查,则克隆响应。 * 这样作的缘由在于,该响应是数据流, 所以主体只能使用一次。 * 因为咱们想要返回能被浏览器使用的响应,并将其传递到缓存以供使用, * 所以须要克隆一份副本。咱们将一份发送给浏览器,另外一份则保留在缓存。 */
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
// 只缓存成功的请求,第三方资源不缓存,固然也能够处理缓存。
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
复制代码
上一部分介绍了 SW 的使用,以及自定义请求响应,实际上,fetch
的玩法有不少,但也都是大同小异。
所以,为了使 SW 更容易使用,GoogleChrome 团队在 Chrome Submit 2017 上首次推出的一套 Web App 静态资源和请求结果本地存储的解决方案 workbox。
来直接感觉下 workbox 的语法
// sw.js。 SW 的注册不变,改变的只是 `sw.js` 的写法
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');
// 不一样的资源使用不一样的缓存策略,并存储在不一样的 storage 中
workbox.routing.registerRoute(
/\.(?:s?html)/,
workbox.strategies.staleWhileRevalidate({
cacheName:'kl-main'
})
);
workbox.routing.registerRoute(
/\.(?:js|css)/,
new workbox.strategies.CacheFirst({
cacheName:'kl-static'
})
);
workbox.routing.registerRoute(
/http:\/\/(?:haitao\.nos\.netease\.com|haitao\.nosdn2\.127\.net)/,
new workbox.strategies.CacheFirst({
cacheName:'kl-cdn'
})
);
复制代码
一看就懂,是否是很简单呢?
这里有个缓存策略,简略介绍一下。
此策略会优先匹配缓存,若是未命中,则透传网络,若是命中则返回缓存响应,同时在后台更新缓存网络响应,此策略比较安全,更像是竞争策略,谁快谁响应。
Network First
网络优先策略,若是网络通畅,返回网络响应并缓存,若是离线,则返回缓存响应
Cache First
缓存优先策略,若是缓存匹配,返回响应,若是不匹配则透传网络,并缓存「有效」响应
Network Only
强制网络响应,即为普通请求
Cache Only
强制缓存响应,无匹配则返回 404
更多关于 workbox 请前往 developers.google.com/web/tools/w…
PWA 另一个爆点就是「可安装」,学名叫做「添加到主屏幕」,这归功于一个配置文件 manifest.json
,它给予开发者自定义图标、显示名称、启动方式等信息并添加至桌面的能力,同时也提供 API 方便开发者管理网络应用安装横幅,让用户能够方便快捷地将站点添加到主屏幕中。
当前 manifest.json 的标准仍属于草案阶段,Chrome、Firefox 和 Apple 都已经实现了这个功能或者功能的部分,微软正努力在 Edge 浏览器上实现。
在 caniuse 中可查到 manifest 的支持度,数据显示 92.64% 的移动浏览器已经达到了支持或者部分支持的程度,想必在不久之后,当规范标准经过后,manifest 的支持度能够达到一个新高度。
我以一个相对完整的 manifest.json
配置文件进行讲解
<!-- 配置由于的引入 -->
<link rel="manifest" href="path-to-manifest/manifest.json">
复制代码
配置介绍
// manifest.json
{
/* 自定义名称 */
"short_name": "短名称",
"name": "这是一个完整名称",
/** 自定义安装 icon * 当PWA添加到主屏幕时,浏览器会根据有效图标的 sizes 字段进行选择。 * 首先寻找与显示密度相匹配而且尺寸调整到 48dp 屏幕密度的图标; * 若是未找到任何图标,则会查找与设备特性匹配度最高的图标; * 若是匹配到的图标路径错误,将会显示浏览器默认 icon。 * * 在启动应用时,启动画面图像会从图标列表中提取最接近 128dp 的图标进行显示 */
"icons": [
{
"src": "path-to-images/icon-96x96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "path-to-images/icon-144x144.png",
"type": "image/png",
"sizes": "144x144"
}
],
/* 设置启动网址 */
"start_url": "index.html",
/** 设置启动背景颜色 * 完整色值 "#0000ff" * 缩写 "#00f" * 预设色值 "blue" * rgb "rgb(0, 0, 255)" * transparent 背景色显示为黑色 */
"background_color": "#0000ff",
/** 设置启动显示类型 * fullscreen 应用的显示界面将占满整个屏幕 * standalone 浏览器相关UI(如导航栏、工具栏等)将会被隐藏 * minimal-ui 显示形式与standalone相似,浏览器相关UI会最小化为一个按钮,不一样浏览器在实现上略有不一样 * browser 浏览器模式,与普通网页在浏览器中打开的显示一致 */
"display": "fullscreen",
/** 指定页面显示方向 * 更多配置介绍:https://lavas.baidu.com/pwa/engage-retain-users/add-to-home-screen/improved-webapp-experience#%E6%8C%87%E5%AE%9A%E9%A1%B5%E9%9D%A2%E6%98%BE%E7%A4%BA%E6%96%B9%E5%90%91 */
"orientation": "landscape",
/* 设置主题颜色 */
"theme_color": "#000",
/** 设置做用域 * start_url 必须在做用域内 */
"scope": "/"
}
复制代码
配置完以后就能够在 Chrome 的 DevTools 中进行验证测试了。
消息推送是 App 保活冲绩效的经常使用手段,因为 HTTP 是一个无状态协议,推送功能在用户关闭了浏览器以后便没了办法,这一次 PWA 赋予了 Web 这个能力。
其中便包含了两个技术点
下面咱们来解析一个通知体
// 消息体的 title
self.addEventListener('push', event => {
const title = "Credit Card";
const options = {
// 主内容
"body": "Did you make a $1,000,000 purchase at Dr. Evil...",
// 视觉配置,如 icon,Badge,image 等,不一样的视觉配置展现的位置也不一样
// 详情参看 https://lavas.baidu.com/pwa/engage-retain-users/notification/notification-display
"icon": "images/ccard.png",
// 震动设置,其中的数字以2个为一组,分别表示震动的毫秒数,和不震动的毫秒数
"vibrate": [200, 100, 200, 100, 200, 100, 400],
// 铃声
"sound": "path/to/sound.mp3",
// 标签,用于客户端消息归类
"tag": "request",
// actions,用户操做后会将结果反馈给浏览器
"actions": [
{ "action": "yes", "title": "Yes", "icon": "images/yes.png" },
{ "action": "no", "title": "No", "icon": "images/no.png" }
]
}
// 激活通知
self.registration..showNotification(title, options);
});
self.addEventListener('notificationclick', event => {
// Do something with the event
event.notification.close();
});
self.addEventListener('notificationclose', event => {
// Do something with the event
});
复制代码
以上的消息配置,展现的结果以下图。
关于推送功能的更多实操不属于本文探究的范畴,有实际需求的同窗能够前往官网进行了解。
传送门>>
developers.google.com/web/fundame…
有人说「PWA 是小程序的祖宗」,不无道理,PWA 对小程序确定存在必定的借鉴意义,可是否会挤压 PWA 的市场?咱们应该放心,小程序的设计并非 Web 的替代者,而是介于原生 App 和 Web 之间的存在。
小程序更倾向于轻便及时触手可得。既没有原生 App 的「沉重」也没有 Web 「迟钝」。在此得天独厚的基础之上加之以「社交流量」的加持,微信小程序的存在并不是偶然。
可是,若是没了网络,同样玩不转;主流的搜索引擎并没有法捕获小程序的内容。因此,App、Web 和小程序是相辅相成的。
另外,笔者想表达另一个观点
「存在即合理,合理未必长久」
在经历过一段痛苦的微信小程序洗礼以后,咱们「欣然」接受了。奈何众XX小程序『竞相开放,争奇斗艳,不亦乐乎』。却不知,我等不才,竟要为了这区区语法之差别,彻夜没法停歇,然,产与出相比,孰轻孰重?
唉~程序员何须为难程序员~~
关于 PWA 的技术早在 2 年前即已相对完整,只是因为「天朝人民太过赋予」,对支持与否未发布意见的 Apple 在天朝市场有着举足轻重的地位,而「外围」仿佛对 Android 机更为推崇,因此,PWA 在国内的发展和推广并不理想。
此时此刻,PWA 的支持度也达到了一个相对让人满意的水平,虽然体验依然没法和原生 App 相提并论,但做为 App 短板的补丁已经是绰绰有余。
因此,『架构师』们,能够盘起来了。
以上就是 PWA 的相关知识点,但愿对你有所帮助。
[1]. developers.google.com/web/fundame…
[2]. developers.google.com/web/fundame…
[3]. developers.google.com/web/tools/w…
[4].下一代 Web 应用模型 —— Progressive Web App
[5]. lavas.baidu.com/
做者:木羽
转载请标明出处