🌞Moon Will Know
🧚🏿

如何构建一个 PWA

什么是 PWA


PWA即 Progressive Web App,渐进式 Web 应用,是由 Google 推广的一种 Web 应用标准,希望借此来结合 Web 与原生 App 的优势。
具有可安装,可离线使用的特性,同时兼具 Web 的轻便和可快速触达。
安装后的 PWA拥有独立的 App运行窗口,作为用户来说,这可以让 Web应用更接近于原生 App。
图片来自 webDev ( 顺便吐槽下 notion 的文字居中居然要通过三栏布局来实现,所以下面的文字很有可能对不齐)
notion image
 
在桌面浏览器中安装
 
notion image
 
在移动端浏览器中安装
 
虽然仅提供作为描述文件的 manifest.json 就可获得可安装特性。但 PWA至少还需要一个 service worker 来提供离线使用能力才能算的上合格。
另外最近不胜其烦的网站通知请求,也是为 PWA 提供了 Notification Api 使其能更接近原生 APP。

如何构建


PWA包括三个必要条件:
  • https 服务,在 localhost 上除外。
  • 至少一个 service worker ,缓存应用的网络请求,控制是否缓存应用的网络请求
  • manifes.json 应用清单,用于描述应用的信息和安装时的 icon。

Service Worker

service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。
service worker 是一个注册在指定源和路径下的事件驱动 worker。它采用 JavaScript 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。
service worker 运行在 worker 上下文,因此它不能访问 DOM。相对于驱动应用的主 JavaScript 线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步 API(如 XHRlocalStorage )不能在 service worker 中使用。
 
  1. 在 public新建 sw.js ( service worker 必须在访问域名的根路径下)。
const version = '1.0.2', CACHE = version + '::ZiyiMember', installFilesEssential = [ '/', '/manifest.json', '/favicon.ico', '/logo.jpeg', ]; // install static assets function installStaticFiles() { return caches.open(CACHE) .then(cache => { return cache.addAll(installFilesEssential); }); } function clearOldCaches() { return caches.keys() .then(keylist => { return Promise.all( keylist .filter(key => key !== CACHE) .map(key => caches.delete(key)) ); }); } self.addEventListener('install', event => { event.waitUntil( installStaticFiles() .then(() => self.skipWaiting()) ); }); self.addEventListener('activate', event => { event.waitUntil( clearOldCaches() .then(() => self.clients.claim()) ); }); self.addEventListener('fetch', event => { if (event.request.method !== 'GET') return; let url = event.request.url; event.respondWith( caches.open(CACHE) .then(cache => { return cache.match(event.request) .then(response => { if (response) { return response; } return fetch(event.request) .then(newreq => { console.log('network fetch: ' + url); if (newreq.ok) cache.put(event.request, newreq.clone()); return newreq; }) .catch(()=>null); }); }) ); });
  1. 在 Web加载时,注册 service worker。
ServiceWorkerContainer.register(scriptURL, options) .then((ServiceWorkerRegistration)=>{ ... })

manifest.json

manifest.json 是每个 WebExtension 唯一必须包含的元数据文件。
通过使用  manifest.json,您可以指定扩展的基本元数据,例如名称和版本,还可以指定扩展各个方面的功能(例如后台脚本,内容脚本和某些浏览器行为)。
它是一个允许使用 "//" 撰写单行注释的、特殊的 JSON 文件。
  1. 编写 manifest.json 文件
示例字段:
{ "browser_specific_settings": { "gecko": { "id": "[email protected]", "strict_min_version": "42.0" } }, "background": { "scripts": ["jquery.js", "my-background.js"], }, "browser_action": { "default_icon": { "19": "button/geo-19.png", "38": "button/geo-38.png" }, "default_title": "Whereami?", "default_popup": "popup/geo.html" }, "commands": { "toggle-feature": { "suggested_key": { "default": "Ctrl+Shift+Y", "linux": "Ctrl+Shift+U" }, "description": "Send a 'toggle-feature' event" } }, "content_security_policy": "script-src 'self' https://example.com; object-src 'self'", "content_scripts": [ { "exclude_matches": ["*://developer.mozilla.org/*"], "matches": ["*://*.mozilla.org/*"], "js": ["borderify.js"] } ], "default_locale": "en", "description": "...", "icons": { "48": "icon.png", "96": "[email protected]" }, "manifest_version": 2, "name": "...", "page_action": { "default_icon": { "19": "button/geo-19.png", "38": "button/geo-38.png" }, "default_title": "Whereami?", "default_popup": "popup/geo.html" }, "permissions": ["webNavigation"], "version": "0.1", "user_scripts": { "api_script": "apiscript.js", }, "web_accessible_resources": ["images/my-image.png"] }
  1. head 中引入
<link rel="manifest" href="/manifest.json" />

Next.js项目提供 PWA支持

通过 next-pwa 这个库,可以快速为 Next.js 项目提供PWA支持。
但是目前使用中发现 SSR 方案将请求代码放在服务端的做法,无法支持请求缓存。而且在安装为 PWA后,请求响应的接口时也会报错。

分析 PWA 是否可用

对于 Chrome 浏览器,在开发者工具中有 Lighthouse 工具可以对 Web 页面的性能、无障碍能力、SEO、PWA 甚至是 框架使用做出建议。
当 PWA 配置不生效的时候,可以使用 Lighthouse 分析,根据建议作出改进。