你在浏览网页的时候,一定会注意到有一些网站在地址栏右侧会显示一个 “加号”,点击这个加号就能安装当前网站的 PWA 应用。是不是很炫酷?我们一起来看一下它的适配实现方法。

什么是 PWA?
PWA(Progressive web apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。
我们平常用浏览器上网,咱们本地有一个代理服务器(就是用来缓存的),把咱们一些常访问的网站的部分资源缓存在本地,等到下一次访问的时候就不用重新 HTTP/HTTPS 请求访问了,加快了访问速度,一段时间后就删除这个缓存。
而 PWA 应用则更近一步,不仅可以自己指定需要缓存的文件和目录,还可以像 App 那样,独立出来、常驻桌面,体验也与传统 App 没有什么大的区别。
废话不多说了,更多的介绍可以去相关网站,都有很详细的讲解:
Google: https://developers.google.com/web/progressive-web-apps/
web.dev: https://web.dev/progressive-web-apps/
mozilla.org: https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps/Introduction
让网站初步支持 PWA
PWA 应用开发有着很大的空间,是一个长期的开发过程。但是让网站初步支持、适配 PWA 还是很容易的,主要有以下几步:
网站的 HTTPS 化
支持 PWA 的必要条件,就是网站必须 HTTPS 化。
然后对网站进行一次检测。你可以使用 Chrome 的 Lighthouse 对网站进行测试。Lighthouse 是一个 chrome 插件,可以告诉你访问的网站是不是支持 PWA,如果不支持应该如何优化。

还可以使用 web.dev 的测试页面:

没有严重问题,就可以开始准备相关配置文件了!
准备文件
清单如下,需要如下三个文件是必要的:
- manifest.json(PWA应用信息的文件)
- sw.js(Service Worker 的配置文件)
- pwa.png(PWA 应用的图标图片)
1.PWA应用图标
建议制作 192×192px 和 512×512px 这两种尺寸的 .png 格式的图标。

2.manifest.json
manifest.json 文件就是一个结构化数据文件,我是这样配置的:
{ "name": "筑暻卖萌屋", "short_name": "筑暻卖萌屋", "description": "Nousbuild Website PWA", "icons": [ { "src": "https://www.nousbuild.org/pwa@192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { "src": "https://www.nousbuild.org/pwa@512.png", "sizes": "512x512", "type": "image/png" } ], "background_color": "#FFE8E7", //应用加载之前的背景色,用于应用启动时的过渡 "theme_color": "#FFE8E7", //主题颜色,用于控制浏览器地址栏着色 "display": "standalone", // 定义应用的显示方式 "orientation": "portrait", "start_url": "/", // 打开后第一个出现的页面地址 "scope": "/" // PWA作用域 }
从上到下,一次是 PWA 应用的名称、描述、图标文件、主题色、显示方式、开始页面的链接和 PWA 的作用域。相信这些都很好懂,大家直接改成自己的就行了,这是最最基本的选项,建议都填。
此外还有很多相关参数可以填写,具体可以参考 web.dev。Github 上有一个开源 Web App Manifest 生成器,你还可以在此填写并生成。
3.sw.js
PWA 是通过 ServiceWorker 访问 Cache,所以需要注册 ServiceWorker 工作者,就用 .js 文件来配置:
'use strict' var cacheStorageKey = 'minimal-pwa-8' let cacheName = 'pwa-you-website-cache'; // 缓存名字 var cacheList = [ // 所需缓存的文件 '/', "index.html" ] self.addEventListener('install', function(e) { console.log('Cache event!') e.waitUntil( // 安装服务者时,对需要缓存的文件进行缓存 caches.open(cacheStorageKey).then(function(cache) { console.log('Adding to Cache:', cacheList) return cache.addAll(cacheList) }).then(function() { console.log('Skip waiting!') return self.skipWaiting() }) ) }) self.addEventListener('activate', function(e) { // 判断地址是不是需要实时去请求,是就继续发送请求 console.log('Activate event') e.waitUntil( Promise.all( caches.keys().then(cacheNames => { return cacheNames.map(name => { if (name !== cacheStorageKey) { return caches.delete(name) } }) }) ).then(() => { console.log('Clients claims.') return self.clients.claim() }) ) }) self.addEventListener('fetch', function(e) { // 匹配到缓存资源,就从缓存中返回数据 e.respondWith( caches.match(e.request).then(function(response) { if (response != null) { console.log('Using cache for:', e.request.url) return response } console.log('Fallback to fetch:', e.request.url) return fetch(e.request.url) }) ) })
三个必要文件准备好了,我们就开始组装使用。
使用文件
将 manifest.json 文件,引入到每个页面的 <header> 中:
<link rel="manifest" href="manifest.json">
先判断浏览器是否支持 PWA,再在每个页面有条件的引入 sw.js 文件:
<script type="text/javascript"> if (navigator.serviceWorker != null) { navigator.serviceWorker.register('sw.js') .then(function(registration) { console.log('Registered events at scope: ', registration.scope); }); } </script>
这样我们就初步的成功适配了 PWA!
问题排查
可能会有小伙伴们问,为什么我照着这样做了,我的网站右上角还是没有 PWA 应用的 “加号”?

首先你现在 Chrome 的 Application 页面下进行排查。看 Manifest 标签有没有读出你的 PWA 应用信息配置文件:

再到下面一个 Service Workers 标签,看看状态码是不是正常的:

如果有一个有问题,就要自己排查看看哪里出错了。如果都正常,但是就是没有 PWA 应用的 “加号”,那可能就是因为这一条:用户需要至少浏览网站两次,并且两次访问间隔在五分钟之上。
Service Workers 的改进
如果你的网站是使用动态语言(例如.php)开发的,那你在访问网站或者某些页面的时候可能会出错,浏览器会提示某页面 “已永久性地移动到了新网址” 或者 “网页可能暂时无法连接” 这样的提示,必须要清空掉 PWA 的缓存,才可以访问,然后又出现这样的错误。

这应该是因为动态文件缓存的原因,导致 PWA 在缓存中没有找到 php 文件。这也是咱们 sw.js 写的不严谨,只是初步适配。改进的 sw.js 可以这样写:
'use strict'; const cacheName = 'pwa-you-website-cache'; // 缓存名字 const startPage = 'https://www.nousbuild.org/'; // 首页地址 const offlinePage = 'https://www.nousbuild.org/';// 离线首页地址 const filesToCache = [startPage, offlinePage]; // 不缓存的目录,比如 WordPress 的 wp-admin 和 wp-login 文件夹 const neverCacheUrls = [/wp-admin/,/wp-login/,/preview=true/]; // 之后的代码可以不用修改 // Install self.addEventListener('install', function(e) { console.log('PWA service worker installation'); e.waitUntil( caches.open(cacheName).then(function(cache) { console.log('PWA service worker caching dependencies'); filesToCache.map(function(url) { return cache.add(url).catch(function (reason) { return console.log('PWA: ' + String(reason) + ' ' + url); }); }); }) ); }); // Activate self.addEventListener('activate', function(e) { console.log('PWA service worker activation'); e.waitUntil( caches.keys().then(function(keyList) { return Promise.all(keyList.map(function(key) { if ( key !== cacheName ) { console.log('PWA old cache removed', key); return caches.delete(key); } })); }) ); return self.clients.claim(); }); // Fetch self.addEventListener('fetch', function(e) { // Return if the current request url is in the never cache list if ( ! neverCacheUrls.every(checkNeverCacheList, e.request.url) ) { console.log( 'PWA: Current request is excluded from cache.' ); return; } // Return if request url protocal isn't http or https if ( ! e.request.url.match(/^(http|https):\/\//i) ) return; // Return if request url is from an external domain. if ( new URL(e.request.url).origin !== location.origin ) return; // For POST requests, do not use the cache. Serve offline page if offline. if ( e.request.method !== 'GET' ) { e.respondWith( fetch(e.request).catch( function() { return caches.match(offlinePage); }) ); return; } // Revving strategy if ( e.request.mode === 'navigate' && navigator.onLine ) { e.respondWith( fetch(e.request).then(function(response) { return caches.open(cacheName).then(function(cache) { cache.put(e.request, response.clone()); return response; }); }) ); return; } e.respondWith( caches.match(e.request).then(function(response) { return response || fetch(e.request).then(function(response) { return caches.open(cacheName).then(function(cache) { cache.put(e.request, response.clone()); return response; }); }); }).catch(function() { return caches.match(offlinePage); }) ); }); // Check if current url is in the neverCacheUrls list function checkNeverCacheList(url) { if ( this.match(url) ) { return false; } return true; }
这样子就可以解决动态网页的问题。
Chrome 93 的问题修复
从Chrome 89(将于2021年3月稳定)开始,如果PWA在离线状态下未提供有效的响应,则会在开发人员工具的“问题”标签下显示一条消息。该 beforeinstallprompt 事件和浏览器内安装提示仍将提供。

而从Chrome 93(将于2021年8月稳定)开始,将强制执行更新的安装条件。如果PWA在脱机时未提供有效的响应,它将不再通过可安装性检查,beforeinstallprompt 将不会触发该事件,并且不会显示浏览器内安装提示。这样我们之前写的 sw.js 文件就无效了。
详见Google 文档:改进渐进式Web App脱机支持检测
最新的 sw.js 改进文件如下:
// PWA应用的版本 const CACHE_VERSION = '1.0.0'; // 常缓存文件,可自由添加 const BASE_CACHE_FILES = [ 'https://www.nousbuild.org/pwa@192.png', 'https://www.nousbuild.org/pwa@512.png' ]; // 离线缓存首页 const OFFLINE_CACHE_FILES = [ 'https://www.nousbuild.org/', ]; // 未找到缓存文件时返回 const NOT_FOUND_CACHE_FILES = [ 'https://www.nousbuild.org/', ]; // 离线缓存首页和未找到缓存文件时返回 const OFFLINE_PAGE = 'https://www.nousbuild.org/'; const NOT_FOUND_PAGE = 'https://www.nousbuild.org/'; /** 后续代码文件不用修改 完整 sw.js 下载地址:https://oss.nousbuild.com/download/sw-example.zip 只需修改上述开头几行配置信息即可 **/
新写的 sw.js 增加了缓存列表,适配了 Chrome 93,你只需修改相关的链接即可使用。
浏览器的支持
最后说一下浏览器的支持。其实 Microsoft Edge 可以直接将你的网站转成 PWA 应用,而 Chrome 必须适配以后,才会有这个选项。Safari 的支持比较一般。
苹果的 Safari 如果仅 manifest.json 配置可能还并不能兼容,还要通过 <meta> 和 <link> 进行设置。可设置如下:
<!-- PWA应用名称 --> <meta name="apple-mobile-web-app-title" content="PWA应用名称"> <!-- 是否隐藏地址栏 --> <meta name="apple-mobile-web-app-capable" content="yes"> <!-- 修改状态栏颜色 --> <meta name="apple-mobile-web-app-status-bar-style" content="black">
iOS 版的 Safari 不支持显示 PWA 适配的提示,只能自己引导用户将网站添加到主屏幕。

收藏了,准备日后搭建
确实,Chrome就这样,谷天天改来改去