弱网神器PWA

PWA简介

全称Progressive Web App,中文意思是渐进式Web App,我习惯称升级版web网页。PWA传统 Web App,Native App 的对比如下:

 

是否可安装

是否可链接访问

用户体验

用户黏性

传统 Web

无法安装

可链接访问

体验一般

黏性差

Native App

可安装

不可链接访问

体验好

黏性强

PWA

可安装

可链接访问

体验好

黏性强

亮点:

  • 隐藏浏览器默认功能,导航栏、工具栏等,给用户沉浸式体验。
  • 添加到桌面(与简单主屏幕链接或书签不同),还可以在断网的情况下使用。
  • 能够推送,甚至离线推送(这里的离线是指用户关闭网页,并非指断网)。

在国内也有很多 PWA 站点,比如饿了么和新浪微博的移动版。

核心技术

PWA是一系列技术的组合,核心主要三块,Web App Manifest,Service Worker和Web Push,这几项技术还在不断进化,所以叫它渐进式web,简单介绍一下这三个东西:

Web App Manifest

为了跟Native App 一样的沉浸式体验,Web App Manifest 允许开发者配置隐藏浏览器多余的 UI,这也是 Native App 相比 Web App 用户黏性更好的原因之一。具体使用方式其实就是编写一个manifest.json,在里面列出应用名称、图标、启动方式、背景颜色、主题颜色等等。详细配置参数见MDN,还可以通过Web App Manifest Generator可视化配置。

目前PWA应用在安卓手机上表现比较好,如果安卓安装了谷歌浏览器的话,对PWA的体验会更充分,安卓还能主动询问是否添加到主屏幕。ios的话我试了谷歌浏览器没有找到添加到屏幕的入口,Safri上是有的,但是一些Manifest配置项需要兼容。

Service Worker

Service Worker 是PWA的心脏,发动机,主要特点如下:

  • 一个特殊的 worker 线程,独立于当前网页主线程,有自己的执行上下文。
  • 可拦截并代理请求和处理返回,可以操作本地缓存,如 CacheStorage,IndexedDB 等
  • 一旦被安装,就永远存在,除非显示取消注册或者更新。
  • 能接受服务器推送的离线消息。

注册

注册SW很简单,如下所示。此时受sw控制的范围是sw文件所在的目录下的所有页面,也可以通过register的第二个参数配置scope自定义作用域,但是最大不能超过sw所在目录范围,否则报错。例如sw.js存在/a/b目录下,那么scope最多能配置成/a,不能配置成/d。

生命周期

 

Service Worker在第一次被注册后,再次访问页面时,在内部都有一系列的工作流程,下图是 Service Worker 工作流程图。

更新原理

前面说了sw注册一次后变永久存在,刷新页面或者关闭后重新打开生命周期钩子都不会重新执行,除非手动注销或者sw文件有更新,浏览器会自动对sw文件做diff对比,发现sw文件名或者内容有变化会重新进行注册、安装,当检测到当前的页面被激活态的SW控制着的话,会进入waiting状态但不会立马激活新,需要等所有的终端都关闭之后,再重新打开页面才能激活新的SW。

Service Worker 在全局提供了一个 skipWaiting() 方法,可以跳过等待。但页面已经注册了sw并且正在访问,这时如果sw文件更新了并且跳过等待,那页面提取的部分数据将由旧 Service Worker 处理,而新 Service Worker 处理后来提取的数据。如果预期到缓存数据不一致的现象会导致问题,则不要使用 skipWaiting() 跳过 waiting 状态。

终端概念

在手机端或者 PC 端浏览器,每新打开一个已经激活了 SW 的页面,那 SW 所控制的终端就新增一个,每关闭一个包含已经激活了 SW 页面的时候,则 SW 所控制的终端就减少一个,如上图打开了三个浏览器标签,则当前 SW 控制了三个终端。

缓存与请求拦截

PWA之所以能够做到离线访问,是因为充分利用了浏览器缓存能力,例如cacheStorage、IndexDB。具体看下怎么结合SW使用cacheStorage:

let dataCacheName = 'new-data-v1'
let cacheName = 'first-pwa-app-1'
let filesToCache = [
  '/',
  '/index.html',
  '/script/index.js',
  '/style/index.css',
  ...
]

self.addEventListener('install', function (e) {
  console.log('SW Install')
  e.waitUntil(
    caches.open(cacheName).then(function (cache) {
      console.log('SW precaching')
      return cache.addAll(filesToCache)
    })
  )
  self.skipWaiting()
})
self.addEventListener('fetch', function (e) {
  console.log('SW Fetch', e.request.url)
  // 如果数据相关的请求,需要请求更新缓存
  let dataUrl = '/mockData/'
  if (e.request.url.indexOf(dataUrl) > -1) {
    e.respondWith(
      caches.open(dataCacheName).then(function (cache) {
        return fetch(e.request).then(function (response){
          cache.put(e.request.url, response.clone())
          return response
        }).catch(function () {
          return caches.match(e.request)
        })
      })
    )
  } else {
    e.respondWith(
      caches.match(e.request).then(function (response) {
        return response || fetch(e.request)
      })
    )
  }
})

消息推送

桌面通知

在 iOS 和 Android 移动设备中,Native App 推送通知很常见,容易引导用户重新访问应用。Web App 一直缺少推送通知的能力,Notification是 HTML5 新增的一套展示桌面通知的 API。

Notification.requestPermission().then(permission => {
    // 通过 permission 判断用户的选择结果
    if (permission === 'granted') {
        console.log('用户已授权,可展示通知');
        const title = 'Notification Title'
        const options = {
            body: 'Simple piece of body text.\nSecond line of body text :)'
        }
        const notification = new Notification(title, options)
    } else if (Notification.permission === 'denied') {
        console.log('用户已禁止');
    } else {
        console.log('用户尚未授权,需首先向用户申请通知权限')
    }
})

例如:https://pwa-push-1fdb6.web.app/

如果在手机上大概是这个样子:

在JS主线程还可以这样发起通知:

 navigator.serviceWorker.getRegistration().then(function (registration) {
    registration.showNotification('你好', {/* options */}).then(function () {
            // 通知展现成功
      })
      .catch(function (e) {
          // 通知展现未授权
      })
})

在sw文件内则是这样发起通知:

self.registration.showNotification(title, {...options}).then(function () {
    console.log(11)
  // 通知展现成功
})
.catch(function (e) {
  // 通知展现未授权
})

消息推送

Notification可以在PC或者手机端创建通知,但是当浏览器没有打开,Service Worker 处于休眠状态时就没办法给用户推送。先了网络推送的基本流程:

  1. UA,即浏览器;
  2. Push Service,即推送服务器,用于管理推送订阅、消息推送等功能的第三方服务器。该服务器是浏览器决定的;但是在本地使用web-push库就可以了
  3. Application Server,即网站应用的后端服务。

 

要想手机或PC收到推送满足下面条件:

1. PC端的Chrome要可以FQ,也就是能够使用谷歌相关的服务。

2. 手机端的Chrome要内置了Chrome服务(GMS),实验国内的华为,vivo,小米系列基本是没有内置Chrome服务的,能够连接Google Play则代表可以。

web-push是基于谷歌的FCM(云消息机制)实现的推送,而FCM包含在GMS里面,但是谷歌禁止国外手机使用谷歌服务。

VAPID

出于用户隐私考虑,在应用和推送服务器之间没有进行强身份验证,解决方案是对 Web Push 使用自主应用服务器标识(VAPID)协议,VAPID 允许应用服务器向推送服务器标识身份,推送服务器知道哪个应用服务器订阅了用户,并确保它也是向用户推送信息的服务器。使用 VAPID 服务过程分为以下几个步骤:

  • 应用服务器创建一对公钥/私钥,并将公钥提供给 Web App 客户端
  • 当用户尝试订阅推送服务时,将公钥添加到 subscribe() 订阅方法中,公钥将被发送到推送服务保存。
  • 应用服务器想要推送消息时,发送包含公钥和已经签名的 JSON Web 令牌到推送服务提供的 API,推送服务验证通过后,将信息推送至 Web App 客户端。

利用web-push生成公钥/私钥

npm install web-push -g

web-push generate-vapid-keys

得到如下:

=======================================

Public Key: BLjmecELgzCq4S-fJyRx9j03wvR0yjSs6O13L6qABrj7CadS8689Lvi2iErzG8SeaPSX_ezoyD2O0MMkGZcj4c0

Private Key: wNY2Jw8Zcw2wjfsiVzIxQB6K-ZoOkn-MS7fXxoo8w0Y =======================================

 

客户端订阅推送服务:

async function subscribe () {
  // 判断兼容性
  if (window.PushManager == null && navigator.serviceWorker == null) {
    return
  }
  // 注册 service-worker.js 获取 ServiceWorkerRegistration 对象
  let registration = await navigator.serviceWorker.register('/service-worker', {scope: '/'})
  // 发起推送订阅
  let pushSubscription = await registration.pushManager.subscribe({
    userVisibleOnly:true,
    applicationServerKey: base64ToUint8Array('Public Key')
  })
  // 将 pushSubscription 发送给应用后端服务器
  await distributePushResource(pushSubscription)
}

后端进行推送:

const webpush = require('web-push')

const vapidKeys = {
  publicKey: 'BLjmecELgzCq4S-fJyRx9j03wvR0yjSs6O13L6qABrj7CadS8689Lvi2iErzG8SeaPSX_ezoyD2O0MMkGZcj4c0',
  privateKey: 'wNY2Jw8Zcw2wjfsiVzIxQB6K-ZoOkn-MS7fXxoo8w0Y'
}
webpush.setVapidDetails(
  'mailto:your-email@provider.com',
  vapidKeys.publicKey,
  vapidKeys.privateKey
)

module.exports = function pushMessage (pushSubscription, message) {
  return webpush.sendNotification(pushSubscription, message)
}
posted @   傻白有点甜  阅读(468)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示