# PWA 初探
js13kpwa 为 MDN 例子
# PWA 介绍
PWA(Progressive web apps, 渐进式 Web 应用):运用现代的 WebAPI 以及传统的渐进式增强策略来创建跨平台的 Web 应用程序。(具有与原生应用相同的用户体验)
# 优势
PWA 可被发现,易安装,可理解,独立于网络,渐进式,可重用,响应性和安全。
- 减少应用安装后的加载时间:用 ServiceWorkers 来进行缓存,以此来节省宽带和时间
- 当应用更新时,可以仅仅更新发生改变的那部分内容。
- 外观和使用感受与原生平台更加融为一体——应用可放置在主屏幕,可以全屏运行 等
- 凭借系统通知和推送消息与用户保持连接,对用户产生更多的吸引力,并提高转换效率
# 什么使应用成为 PWA
ServiceWorker
,
Manifest
,
Push
,
Notification
,
A2HS
PWA 不是使用一种技术创建的,而是代表构建 Web 应用程序的新理念,涉及一些特定的模式,API 和其他功能。
当一个程序满足某些要求的时候就可以视为 PWA,或实现给定功能(离线工作,可安装,易于同步,可推送通知等),则可视为 PWA.
可以用工具粗略的检测网站指标:Lighthouse
(谷歌插件)。
在线生成 PWA: https://www.pwabuilder.com/
;
# 成为 PWA 应用需要以下关键原则特点:
- Discoverable: 内容可通过搜索引擎发现
- Installable: 可以出现在设备的主屏幕上
- Linkable: 可用 URL 来分享它
- Network independent: 可在离线状态或网速很差的情况下运行
- Progressive: 在老板浏览器可以使用,在新版浏览器可以使用全部功能
- Re-enageable: 无论何时有新的内容都可发送通知
- Responsive: 在任何有屏幕和浏览器的设备上都可以使用——包括手机,平板电脑,笔记本,电脑,冰箱等
- Safe: 用户和应用之间的连接是安全的,可以阻止第三方访问你的敏感数据
# PWA 结构
# 应用的架构
渲染网站主要有两种方法-》在服务器上 或 在客户端上。
- 服务器端渲染(SSR):在服务器上渲染网页,因此首次加载会更快,但在不同页面之间导航需要下载新的 HTML,能在浏览器中运行良好,但它收到加载速度的制约,因而带来可感知的性能延迟-》加载一个页面需要和服务器之间一次往返。
- 客户端渲染(CSR):允许在导航到不同页面时几乎立即在浏览器中更新网站,但在开始时需要更多的初始化下载和客户端上的额外渲染。首次访问慢,后续访问快。
将 SSR 和 CSR 混合使用可获得最佳的结果-》在服务器上渲染网页,缓存其内容,然后在客户端需要时更新渲染。由于 SSR,第一页加载很快。因为客户端可以仅使用已更改的部分重新渲染页面,所以页面之间的导航是平滑的。(app shell)
# App shell(程序的外壳)
即加载资源(缓存到本地的-未缓存的去请求)以尽快的绘制最小(或最基础)的用户界面。
# ServiceWorker 生命这期
install => waiting => acivate => fetch
# ServiceWorker 两大禁忌
1, sw.js 要使用相同的名字,因为 sw.js 本身就在 html 里,html 会被缓存到 cache,这是一个死循环,永远都不会用到最新的 js; 2,不要给 sw.js 设置缓存,理由同 1
# 新的 sw 需要接管页面的方法
sw 不会通过页面刷新或简单的切换页面更新的,可以通过 skipWaiting 强制更新
# 一:skipWaiting
self.addEventListener("install", (event) => {
// 让新的SW插队,强制令他立刻取代老的sw
self.skipWaiting();
});
缺点:像断网或网络不顺畅或采用 CacheFirst 之类的缓存策略的时候,当老的 sw 在请求了一半资源,突然发现有新的 sw,老的被干掉,新的接管,给页面添加了很多不稳定因素。
# 二:skipWaiting+刷新
在注册 sw 的地方,通过 controllerchange 事件来得知控制当前页面的 sw 是否发生变化
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/js13kpwa/sw.js", {
scope: ".",
})
.then(function(registration) {
console.log("pwa registered", registration);
});
let refreshing = false; // 避免无限刷新
navigator.seviceWorker.addEventListener("controllerchange", function(event) {
// 或者直接刷新
// window.location.reload();
// if(refreshing){
// return;
// }
// refreshing = true;
// window.location.reload();
console.log("Controllerchange, ServiceWorker: ", event);
navigator.serviceWorker.controller.addEventListener(
"statechange",
function() {
console.log("statechange: ", this.state);
if (this.state === "activated") {
document
.getElemeentById("offlineNotification")
.classList.remove("hidden");
}
}
);
});
}
# 三:给用户一个提示
给用户一个提示,由用户点击更新 SW,并引发刷新。 大致流程: 1:浏览器检测到存在新的(不同的)sw,安装并让它等待,同时触发 updatefound 事件 2:监听事件里弹出一个提示信息,询问用户要不要更新 sw 3:如果用户确认,则向处在等待的 sw 发送消息,要求其执行 skipWaiting 并取得控制权(需要使用 postMessage) 4:因为 sw 变化而触发了 controllerchange 事件,在这个事件上刷新即可
# 第二步
function emitUpdate() {
var event = document.createEvent("Event");
// 发送名为"sw.update"的一个事件来通知外部
event.initEvent("sw.update", true, true);
window.dispatchEvent(event);
}
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/service-worker.js")
.then(function(reg) {
if (reg.waiting) {
emitUpdate();
return;
}
reg.onupdatefound = function() {
var installingWorker = reg.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case "installed":
if (navigator.serviceWorker.controller) {
emitUpdate();
}
break;
}
};
};
})
.catch(function(e) {
console.error(e);
});
}
# 第三部
// 用户点击事件
try {
navigator.serviceWorker.getRegistration().then((reg) => {
reg.waiting.postMessage("skipWaiting");
});
} catch (e) {
window.location.reload();
}
// sw.js
self.addEventListener("message", (event) => {
if (event.data === "skipWaiting") {
self.skipWaiting();
}
});
// controllerchange
# 通过 ServiceWorkers 让 PWA 离线工作
# 让 PWA 易于安装
header
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<meta
content="width=device-width,initial-scale=1,user-scalable=no"
name="viewport"
/>
<meta name="description" content="" />
<meta name="keywords" content="" />
<link rel="shortcut icon" href="xxx.ico" type="image/x-icon" />
<link rel="manifest" href="manifest.json" />
<!-- Standard iPhone -->
<link rel="apple-touch-icon" sizes="57x57" href="static/logo/57.png" />
<!-- Retina iPhone -->
<link rel="apple-touch-icon" sizes="114x114" href="static/logo/114.png" />
<!-- Standard iPad -->
<link rel="apple-touch-icon" sizes="72x72" href="static/logo/72.png" />
<!-- Retina iPad -->
<link rel="apple-touch-icon" sizes="144x144" href="static/logo/144.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="format-detection" content="telephone=no,address=no" />
<meta name="apple-mobile-web-app-status-bar-style" content="white" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="referrer" content="no-referrer" />
<title></title>
<script src="https://file.51meeting.com/sit/services.js"></script>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("./service-wroker.js")
.then((registration) =>
console.log(
"ServiceWorker 注册成功!作用域为: ",
registration.scope
)
)
.catch((err) => console.log("ServiceWorker 注册失败: ", err));
}
</script>
</head>
<body>
<div id="app"></div>
<script>
!(function(x) {
function w() {
var v,
u,
t,
tes,
s = x.document,
r = s.documentElement,
a = r.getBoundingClientRect().width;
if (!v && !u) {
var n = !!x.navigator.appVersion.match(/AppleWebKit.*Mobile.*/);
v = x.devicePixelRatio;
tes = x.devicePixelRatio;
(v = n ? v : 1), (u = 1 / v);
}
if (a >= 640) {
r.style.fontSize = "40px";
} else {
if (a <= 320) {
r.style.fontSize = "20px";
} else {
r.style.fontSize = (a / 320) * 20 + "px";
}
}
}
x.addEventListener("resize", function() {
w();
});
w();
})(window);
</script>
</body>
</html>
# A2HS(Add to Home screen)
例子: 不依赖浏览器,不必每次都弹出 A2HS 的 banner,由用户控制,点击按钮提示安装
let installPromptEvent;
// 监听beforeinstallprompt事件,浏览器触发A2HS时会执行
window.addEventListener("beforeinstallprompt", (e) => {
// 阻止自动提示
e.preventDefault();
// 储存事件对象,方便在之后的按钮事件中手动触发
installPromptEvent = e;
});
// ui 上的按钮点击事件
installBtn.addEventListener("click", (e) => {
// 弹出A2HS
installPromptEvent.prompt();
installPromptEvent.userChoice.then((choiceResult) => {
if (choiceResult.outcome === "accepted") {
console.log("accepted");
} else {
console.log("refuse");
}
installPromptEvent = null;
});
});
# 推送和通知功能
这是两个 api