什么是 PWA 应用
渐进式 Web 应用(Progressive Web App, PWA)是一个使用 web 平台技术构建的应用程序,但它提供的用户体验就像一个特定平台的应用程序。
它像网站一样,PWA 可以通过一个代码库在多个平台和设备上运行。它也像一个特定平台的应用程序一样,可以安装在设备上,可以离线和在后台运行,并且可以与设备和其他已安装的应用程序集成。
离线访问和安装涉及到两个技术 Service Worker 和 Web App Manifest
Service Worker
什么是 Service Worker
Service worker 是一个注册在指定源和路径下的事件驱动 worker。它采用 JavaScript 文件的形式,控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。
Service worker 运行在 worker 上下文:因此它无法访问 DOM,相对于驱动应用的主 JavaScript 线程,它运行在其他线程中,所以不会造成阻塞。它被设计为完全异步;因此,同步 XHR 和 Web Storage 不能在 service worker 中使用。
Service Worker 生命周期
Service Worker 的生命周期包括以下几个阶段:
- 注册
- 安装
- 等待
- 激活
- 终止
在等待阶段,新的 Service Worker 需要等待所有控制它的页面(也就是使用它的页面)都被关闭后才能进入激活阶段。这是因为如果在一个页面还在使用旧的 Service Worker 的时候就激活新的 Service Worker,可能会导致问题。
例如,新的 Service Worker 可能会使用一个新的缓存策略,而这个页面可能还在使用旧的缓存策略,这可能会导致缓存的不一致。
等待所有页面都被关闭可以确保在新的 Service Worker 激活时,没有页面还在使用旧的 Service Worker。这样,我们就可以在新的 Service Worker 中自由地更新我们的缓存策略,而不需要担心会影响到正在使用旧的 Service Worker 的页面。
如果你希望新的 Service Worker 在安装完成后立即激活,无需等待所有页面都被关闭,你可以在你的 Service Worker 脚本中调用 self.skipWaiting() 方法。但是,请注意这可能会导致上述的问题。
创建 sw.js
Service Worker 只能控制其所在目录及其子目录下的文件。
例如,如果你将 sw.js 放在 /scripts/ 目录下,那么这个 Service Worker 只能控制 /scripts/ 目录及其子目录下的文件,不能控制其他目录下的文件。
所以如果你希望你的 Service Worker 能控制你的整个站点,那么你应该将其放在项目的根目录
先在 public 目录下创建 sw.js
const CACHE_NAME = "my-cache-v1";
const defaultCacheUrl = ['/background.webp'];
// 当 Service Worker 被首次注册或者更新时,install 事件会被触发。
self.addEventListener("install", function (event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log("Opened cache");
return cache.addAll(urlsToCache);
})
);
});
// Service Worker 被激活时会触发
self.addEventListener("activate", function (event) {
var cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
缓存策略
- Stale while revalidate:这种策略首先从缓存中获取响应并立即返回,然后在后台从网络获取响应并更新缓存。这种策略适用于可以容忍短暂过期的资源。
- Network first, then cache:这种策略首先尝试从网络获取响应,如果网络请求失败,那么它会从缓存中获取响应。这种策略适用于需要实时数据的应用。
- Cache first, then network:这种策略首先尝试从缓存中获取响应,如果缓存中没有找到响应,那么它会从网络获取响应。这种策略适用于静态资源,如 CSS 和 JavaScript 文件。
Stale while revalidate
在 sw.js 中实现 Stale while revalidate 缓存策略
// 直接从缓存中取,同时会发起网络请求 更新本地缓存,这意味着资源不会立即更新,而是会在发起第二次请求后才是最新的
self.addEventListener("fetch", function (event) {
event.respondWith(
caches.open(CACHE_NAME).then(function (cache) {
return cache.match(event.request).then(function (response) {
const fetchPromise = fetch(event.request).then(function (networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
});
注册 Service Worker
最后在main.js中注册 Service Worker
// main.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
console.log('Service Worker registered with scope:', registration.scope);
}).catch(function(error) {
console.log('Service Worker registration failed:', error);
});
}
ok,现在只要用户访问过一次我的网站,第二次访问时即使没网络也可以访问我的页面了
因为所有文件都被缓存了
制作可安装的 PWA
可安装的前置条件
要制作可安装的 PWA需要网站满足 PWA 的所有要求,例如:
- 网站是通过 HTTPS 提供的,本地使用localhost可正常工作
- 网站已经注册了一个 Service Worker,并且可以离线工作
- 网站有一个有效的 Web App Manifest
现在咱们已经通过 Service Worker 实现了离线可访问,接着再创建 Web App Manifest 就可以了
Web App Manifest
Web App Manifest 在一个 JSON 文本文件中提供有关应用程序的信息(如名称,作者,图标和描述)。manifest 的目的是将 Web 应用程序安装到设备的主屏幕,为用户提供更快的访问和更丰富的体验。
在 public 下创建 manifest.json 文件
{
"name": "熬夜佩奇的 PWA 应用",
"short_name": "这是一个 PWA 应用",
"icons": [
{
"src": "/icon-32.png",
"type": "image/png",
"sizes": "32x32"
},
{
"src": "/icon-144.png",
"type": "image/png",
"sizes": "144x144"
}
],
"start_url": "http://localhost:5173",
"display": "standalone"
}
上面的icon的尺寸一定要对应上,否则不生效
然后在 index.html 中添加下面这行代码引入 manifest.json
<link rel="manifest" href="/manifest.json">
这时候打开网站就能安装了,打开网站会在地址栏右侧展示一个安装的图标,chorme浏览器长这样
说实话这个图标有点隐蔽,很容易被用户忽略,这时候咱们可以自定义安装时机
自定义触发安装时机
首先监听 beforeinstallprompt 事件,这个事件会在满足 PWA 条件(见上文)的时候触发,这里全局监听这个事件,然后把事件对象保存起来
const eventRef = ref(null)
window.addEventListener("beforeinstallprompt", function (e) {
e.preventDefault(); // Prevents prompt display
eventRef.value = e;
});
比如当用户点击一个按钮的时候,调用事件对象的 prompt 方法
const add = () => {
eventRef.value.prompt();
}
这时候就会出现安装弹窗了