如何使用推送通知将 WebApp 转换为 PWA

将你的网站转变为 PWA 并加入推送通知

本文将深入探讨如何运用 Firebase 云消息传递服务,将普通的 Web 应用程序或网站升级为具备推送通知功能的 PWA(渐进式 Web 应用)。

在当今互联网时代,将 Web 应用转化为 PWA 是一种潮流,因其能带来离线支持、推送通知和后台同步等强大功能。PWA 的这些特性使得 Web 应用更接近原生应用,从而显著提升用户体验。

许多大型企业,如 Twitter 和亚马逊,已经成功地将其 Web 应用转变为 PWA,以提升用户参与度。

什么是 PWA?

PWA 可以简单理解为: (Web 应用) + (部分原生应用功能)。

本质上,PWA 依然是你的 Web 应用(HTML+CSS+JS),在所有浏览器上的运行方式与之前并无二致。然而,当在现代浏览器中加载时,它能拥有原生应用的一些特性。这不仅增强了 Web 应用的功能,还提高了其可扩展性。例如,我们可以预先获取并缓存前端资源,减少对后端服务器的请求。

PWA 与传统 Web 应用的区别

  • 可安装性:你的 Web 应用现在可以像原生应用一样被安装到设备上。
  • 渐进式:它仍然是你的 Web 应用,但额外具备了一些原生应用的功能。
  • 原生应用体验:一旦安装,用户可以像使用原生应用一样浏览和操作你的 Web 应用。
  • 易于访问:用户不再需要在每次访问时输入网址。安装后,只需轻轻点击即可打开应用。
  • 应用缓存:在 PWA 出现之前,Web 应用唯一的缓存机制依赖于浏览器提供的 HTTP 缓存。而 PWA 允许我们使用客户端代码来缓存内容,这在传统 Web 应用中是无法实现的。
  • 应用商店发布:PWA 可以发布到 Google Play 商店和 iOS App Store。

将你的应用升级为 PWA 无疑会使其更加强大。

为什么企业应该考虑采用 PWA?

通常,客户会先要求开发 Web 应用,然后才考虑 Android 和 iOS 应用。这意味着我们需要一个独立的团队,在 Web 应用中构建与 Android/iOS 应用相同的功能,这无疑增加了开发成本和上市时间。

然而,部分客户预算有限,或者认为上市时间至关重要。

PWA 的功能足以满足大多数客户端的需求。对于这部分客户,我们建议直接使用 PWA,如果他们希望在应用商店发布,则可以通过 TWA 将 PWA 转化为 Android 应用。

当然,如果你的需求确实需要 PWA 无法满足的原生应用功能,那么客户可以根据自己的意愿选择开发原生应用。但即使如此,他们也可以先在应用商店部署 PWA,直到原生应用开发完成。

例如,Titan Eyeplus 最初开发了一个 PWA 应用,并使用 TWA(可信 Web 活动)将其发布到 Play 商店。在他们完成 Android 应用开发后,他们又发布了真正的 Android 应用。通过 PWA,他们成功缩短了上市时间,并降低了开发成本。

PWA 的主要功能

PWA 为 Web 应用带来了类似于原生应用的功能。

主要功能包括:

  • 可安装性:如同原生应用一样安装的 Web 应用。
  • 缓存:允许应用进行缓存,实现离线支持。
  • 推送通知:允许服务器发送推送通知,吸引用户访问网站。
  • 地理围栏:通过事件通知,应用可以感知设备位置变化。
  • 支付请求:允许在应用中启用支付功能,提供更好的用户体验。

未来还将有更多功能加入。

其他功能还包括:

  • 快捷方式:在清单文件中添加的可快速访问的 URL。
  • Web Share API:使你的应用能够接收来自其他应用的共享数据。
  • 徽章 API:在已安装的 PWA 中显示通知计数。
  • 定期后台同步 API:保存用户数据,直到设备连接到网络。
  • 联系人选择器:用于从用户手机中选择联系人。
  • 文件选择器:用于访问本地系统/移动设备上的文件。

PWA 相较于原生应用的优势

虽然原生应用在性能和功能上可能更胜一筹,PWA 仍具备一些显著的优势:

  • PWA 跨平台运行,包括 Android、iOS 和桌面。
  • PWA 降低了开发成本。
  • PWA 的功能部署比原生应用更容易。
  • PWA(网站)对 SEO 友好,更容易被用户发现。
  • PWA 仅通过 HTTPS 提供,更加安全。

PWA 相较于原生应用的劣势

  • 与原生应用相比,PWA 可用的功能有限。
  • PWA 的功能不一定能兼容所有设备。
  • PWA 的品牌知名度较低,因为它不在应用商店中直接展示。

你可以使用 Android 的可信网络活动 (TWA),将你的 PWA 发布为 Android 应用,以此来帮助推广你的品牌。

将 Web 应用转化为 PWA 所需的组件

将任何 Web 应用或网站转化为 PWA,你需要以下组件:

  • Service Worker:PWA 的核心组件,负责缓存和推送通知,它是我们请求的代理。
  • 清单文件:包含 Web 应用详细信息的 JSON 文件,例如应用名称和图标。
  • 应用徽标:高分辨率的应用图标,通常为 512 x 512 像素,PWA 需要在主屏幕和启动画面上显示应用徽标,请使用任何工具创建一套 1:1 比例的图像。
  • 响应式设计:确保 Web 应用能够在不同屏幕尺寸上良好显示。

什么是 Service Worker?

Service Worker 是一种在浏览器后台运行的客户端脚本,它充当 Web 应用和外部世界之间的代理,提供推送通知并支持缓存功能。

Service Worker 独立于主 JavaScript 线程运行,这意味着它无法直接访问 DOM API。它仅可访问 索引数据库 API, 获取 API, 缓存存储 API。但它可以通过消息与主线程通信。

Service Worker 提供的服务:

  • 拦截来自源域的 HTTP 请求。
  • 接收来自服务器的推送通知。
  • 实现应用的离线可用性。

Service Worker 控制着你的应用,并且可以操作请求,但它独立运行。因此,必须使用 HTTPS 来启用源域,以避免中间人攻击。

什么是清单文件?

清单文件 (manifest.json) 包含了关于 PWA 应用的详细信息,用于告知浏览器。

  • name:应用的名称。
  • short_name:应用的简称(如果提供)。如果同时指定了名称和简称,浏览器将优先使用简称。
  • description:对应用的描述。
  • start_url:指定 PWA 启动时的主页。
  • icons:用于主屏幕等位置的 PWA 图标集。
  • background_color:设置 PWA 应用启动画面的背景颜色。
  • display:自定义浏览器 UI 在 PWA 应用中的显示方式。
  • theme_color:PWA 应用的主题颜色。
  • scope:指定 PWA 应用的 URL 范围,默认为清单文件所在的目录。
  • shortcuts:PWA 应用的快捷链接。

将 Web 应用转化为 PWA

为了演示,我创建了一个包含静态文件的 techblik.com 网站文件夹结构。

  • index.html – 主页
  • 文章/
    • index.html – 文章页面
  • 作者/
    • index.html – 作者页面
  • 工具/
    • index.html – 工具页面
  • 交易/
    • index.html – 交易页面

如果你已经拥有一个网站或 Web 应用,请按照以下步骤将其转化为 PWA。

为 PWA 创建所需的图像

首先,获取你的应用徽标,并将其裁剪为 5 种不同的尺寸(1:1 比例)。我使用了 https://tools.crawlink.com/tools/pwa-icon-generator/ 来快速生成不同尺寸的图像,你也可以使用它。

创建清单文件

其次,使用你的应用信息为 Web 应用创建一个 manifest.json 文件。以下是一个为 techblik.com 创建的示例清单文件:

  {
    "name": "techblik.com",
    "short_name": "techblik.com",
    "description": "techblik.com produces high-quality technology & finance articles, makes tools, and APIs to help businesses and people grow.",
    "start_url": "/",
    "icons": [{
      "src": "assets/icon/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    }, {
      "src": "assets/icon/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    }, {
      "src": "assets/icon/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    }, {
      "src": "assets/icon/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    }, {
      "src": "assets/icon/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }],
    "background_color": "#EDF2F4",
    "display": "standalone",
    "theme_color": "#B20422",
    "scope": "/",
    "shortcuts": [{
        "name": "Articles",
        "short_name": "Articles",
        "description": "1595 articles on Security, Sysadmin, Digital Marketing, Cloud Computing, Development, and many other topics.",
        "url": "https://geekflare.com/articles",
        "icons": [{
          "src": "/assets/icon/icon-152x152.png",
          "sizes": "152x152"
        }]
      },
      {
        "name": "Authors",
        "short_name": "Authors",
        "description": "techblik.com - Authors",
        "url": "/authors",
        "icons": [{
          "src": "/assets/icon/icon-152x152.png",
          "sizes": "152x152"
        }]
      },
      {
        "name": "Tools",
        "short_name": "Tools",
        "description": "techblik.com - Tools",
        "url": "https://techblik.com.com/tools",
        "icons": [{
          "src": "/assets/icon/icon-152x152.png",
          "sizes": "152x152"
        }]
      },
      {
        "name": "Deals",
        "short_name": "Deals",
        "description": "techblik.com - Deals",
        "url": "/deals",
        "icons": [{
          "src": "/assets/icon/icon-152x152.png",
          "sizes": "152x152"
        }]
      }
    ]
  }
  

注册 Service Worker

在根文件夹中创建两个 JavaScript 文件:register-service-worker.js 和 service-worker.js。

register-service-worker.js 在主线程上运行,可以直接访问 DOM API。而 service-worker.js 是一个独立的 Service Worker 脚本,拥有较短的生命周期。每当事件触发 Service Worker 时,它会运行,直到完成处理。

通过在主线程的 JavaScript 文件中检查,你可以确定 Service Worker 是否已经注册。如果没有,则需要注册 Service Worker 脚本 (service-worker.js)。

将以下代码粘贴到 register-service-worker.js 文件中:

  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
      navigator.serviceWorker.register('/service-worker.js');
    });
  }
  

将以下代码粘贴到 service-worker.js 文件中:

  self.addEventListener('install', (event) => { // event when service worker install
    console.log( 'install', event);
    self.skipWaiting();
  });

  self.addEventListener('activate', (event) => { // event when service worker activated
    console.log('activate', event);
    return self.clients.claim();
  });

  self.addEventListener('fetch', function(event) { // HTTP request interceptor
    event.respondWith(fetch(event.request)); // send all http request without any cache logic
    /*event.respondWith(
        caches.match(event.request).then(function(response) {
            return response || fetch(event. request);
        })
    );*/ // cache new request. if already in cache serves with the cache.
  });
  

我们在这里并没有重点介绍如何启用缓存来支持离线功能。我们主要讨论如何将 Web 应用转化为 PWA。

在 HTML 页面的 head 标签中添加清单文件和脚本:

  <link rel="manifest" href="https://techblik.com.com/manifest.json">
  <script src="/register-service-worker.js"></script>
  

添加后刷新页面。现在,你可以在移动 Chrome 上安装你的应用了。

应用已经被添加到了主屏幕。

如果你正在使用 WordPress,可以尝试使用现有的 PWA 转换器插件。对于 VueJS 或 ReactJS,你可以按照上述方法操作,或者使用现有的 PWA npm 模块来加速开发,因为 PWA npm 模块通常已经包含了离线支持和缓存等功能。

启用推送通知

Web 推送通知可以帮助我们更频繁地与用户互动。我们可以通过以下 API 来启用它:

  • 通知 API:用于配置推送通知如何展示给用户。
  • 推送 API:用于接收从服务器发送到浏览器的通知消息。

启用推送通知的第一步是检查通知 API 并获得用户授权。将以下代码粘贴到 register-service-worker.js 文件中:

  if ('Notification' in window && Notification.permission != 'granted') {
    console.log('Ask user permission')
    Notification.requestPermission(status => {  
        console.log('Status:'+status)
        displayNotification('Notification Enabled');
    });
  }


  const displayNotification = notificationTitle => {
    console.log('display notification')
    if (Notification.permission == 'granted') {
      navigator.serviceWorker.getRegistration().then(reg => {
        console.log(reg)
        const options = {
          body: 'Thanks for allowing push notification !',
          icon:  '/assets/icons/icon-512x512.png',
          vibrate: [100, 50, 100],
          data: {
            dateOfArrival: Date.now(),
            primaryKey: 0
          }
        };
    
        reg.showNotification(notificationTitle, options);
      });
    }
  };
  

如果一切顺利,你会收到来自应用的通知。

window 中的“通知”表明当前浏览器支持通知 API。Notification.permission 可以告知用户是否允许显示通知。如果用户允许,值将为“granted”,如果用户拒绝,值将为“denied”。

启用 Firebase 云消息传递并创建订阅

现在进入关键部分。为了从服务器向用户推送通知,我们需要为每个用户生成一个唯一的端点/订阅。为此,我们将使用 Firebase 云消息传递。

首先,通过访问 https://firebase.google.com/ 创建一个 Firebase 账户,然后点击 “开始”。

  1. 创建一个新项目并命名,然后点击“继续”。我将使用名称 techblik.com 来创建它。
  2. 在下一步中,默认启用 Google Analytics。你可以选择不启用(我们现在不需要),然后点击 “继续”。如有需要,你可以在稍后在 Firebase 控制台中启用它。
  3. 项目创建后,将会显示如下界面。

然后,进入项目设置,点击“云消息传递”,并生成密钥。

通过以上步骤,你将获得 3 个密钥:

  • 项目服务器密钥
  • Web 推送证书私钥
  • Web 推送证书公钥

现在,将以下代码粘贴到 register-service-worker.js 文件中:

  const updateSubscriptionOnYourServer = subscription => {
      console.log('Write your ajax code here to save the user subscription in your DB', subscription);
      // write your own ajax request method using fetch, jquery, axios to save the subscription in your server for later use.
  };

  const subscribeUser = async () => {
    const swRegistration = await navigator.serviceWorker.getRegistration();
    const applicationServerPublicKey = 'BOcTIipY07N4Y63Y-9r7NMoJHofmCzn3Pu9g-LMsgIMGH4HVr42_LW9ia0lMr68TsTLKS3UcdkE3IcC52hJDYsY'; // paste your webpush certificate public key
    const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
    swRegistration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey
    })
    .then((subscription) => {
      console.log('User is subscribed newly:', subscription);
        updateSubscriptionOnServer(subscription);
    })
    .catch((err) => {
      if (Notification.permission === 'denied') {
        console.warn('Permission for notifications was denied')
      } else {
          console.error('Failed to subscribe the user: ', err)
      }
    });
  };
  const urlB64ToUint8Array = (base64String) => {
      const padding = '='.repeat((4 - base64String.length % 4) % 4)
      const base64 = (base64String + padding)
          .replace(/-/g, '+')
          .replace(/_/g, '/')

      const rawData = window.atob(base64);
      const outputArray = new Uint8Array(rawData.length);

      for (let i = 0; i < rawData.length; ++i) {
          outputArray[i] = rawData.charCodeAt(i);
      }
      return outputArray;
  };

  const checkSubscription = async () => {
      const swRegistration = await navigator.serviceWorker.getRegistration();
      swRegistration.pushManager.getSubscription()
      .then(subscription => {
          if (!!subscription) {
              console.log('User IS Already subscribed.');
              updateSubscriptionOnYourServer(subscription);
          } else {
              console.log('User is NOT subscribed. Subscribe user newly');
              subscribeUser();
          }
      });
  };

  checkSubscription();
  

将以下代码粘贴到 service-worker.js 文件中:

  self.addEventListener('push', (event) => {
    const json = JSON.parse(event.data.text())
    console.log('Push Data', event.data.text())
    self.registration.showNotification(json.header, json.options)
  });
  

至此,前端设置完成。通过获取到的订阅信息,你可以随时向用户发送推送通知,直到他们取消订阅服务。

从 Node.js 后端推送通知

你可以使用 web-push npm 模块来简化操作。

以下是从 Node.js 服务器发送推送通知的代码片段示例:

const webPush = require('web-push');
// pushSubscription is nothing but subscription that you sent from your front-end to save it in DB
const pushSubscription = {"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/gAAAAABh2…E0mTFsHtUqaye8UCoLBq8sHCgo2IC7UaafhjGmVCG_SCdhZ9Z88uGj-uwMcg","keys":{"auth":"qX6AMD5JWbu41cFWE3Lk8w","p256dh":"BLxHw0IMtBMzOHnXgPxxMgSYXxwzJPxpgR8KmAbMMe1-eOudcIcUTVw0QvrC5gWOhZs-yzDa4yKooqSnM3rnx7Y"}};
//your web certificates public-key
const vapidPublicKey = 'BOcTIipY07N4Y63Y-9r7NMoJHofmCzn3Pu9g-LMsgIMGH4HVr42_LW9ia0lMr68TsTLKS3UcdkE3IcC52hJDYsY';
//your web certificates private-key
const vapidPrivateKey = 'web-certificate private key';

var payload = JSON.stringify({
  "options": {
    "body": "PWA push notification testing fom backend",
    "badge": "/assets/icon/icon-152x152.png",
    "icon": "/assets/icon/icon-152x152.png",
    "vibrate": [100, 50, 100],
    "data": {
      "id": "458",
    },
    "actions": [{
      "action": "view",
      "title": "View"
    }, {
      "action": "close",
      "title": "Close"
    }]
  },
  "header": "Notification from techblik.com-PWA Demo"
});

var options = {
    vapidDetails: {
      subject: 'mailto:[email protected]',
      publicKey: vapidPublicKey,
      privateKey: vapidPrivateKey
    },
    TTL: 60
  };

webPush.sendNotification(
    pushSubscription,
    payload,
    options
).then(data => {
    return res.json({status : true, message : 'Notification sent'});
}).catch(err => {
    return res.json({status : false, message : err });
});

以上代码将向订阅用户发送推送通知。Service Worker 中的 `push` 事件将会被触发。

从 PHP 后端推送通知

对于 PHP 后端,你可以使用 web-push-php composer 包。以下是一个发送推送通知的示例代码:

  <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

  require __DIR__.'/../vendor/autoload.php';
  use MinishlinkWebPushWebPush;
  use MinishlinkWebPushSubscription;

  // subscription stored in DB
  $subsrciptionJson = '{"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/gAAAAABh2…E0mTFsHtUqaye8UCoLBq8sHCgo2IC7UaafhjGmVCG_SCdhZ9Z88uGj-uwMcg","keys":{"auth":"qX6AMD5JWbu41cFWE3Lk8w","p256dh":"BLxHw0IMtBMzOHnXgPxxMgSYXxwzJPxpgR8KmAbMMe1-eOudcIcUTVw0QvrC5gWOhZs-yzDa4yKooqSnM3rnx7Y"}}';
  $payloadData = array (
    'options' =>  array (
                    'body' => 'PWA push notification testing fom backend',
                    'badge' => '/assets/icon/icon-152x152.png',
                    'icon' => '/assets/icon/icon-152x152.png',
                    'vibrate' => 
                    array (
                      0 => 100,
                      1 => 50,
                      2 => 100,
                    ),
                    'data' => 
                    array (
                      'id' => '458',
                    ),
                    'actions' => 
                    array (
                      0 => 
                      array (
                        'action' => 'view',
                        'title' => 'View',
                      ),
                      1 => 
                      array (
                        'action' => 'close',
                        'title' => 'Close',
                      ),
                    ),
    ),
    'header' => 'Notification from techblik.com-PWA Demo',
  );

  // auth
  $auth = [
      'GCM' => 'your project private-key', // deprecated and optional, it's here only for compatibility reasons
      'VAPID' => [
          'subject' => 'mailto:[email protected]', // can be a mailto: or your website address
          'publicKey' => 'BOcTIipY07N4Y63Y-9r7NMoJHofmCzn3Pu9g-LMsgIMGH4HVr42_LW9ia0lMr68TsTLKS3UcdkE3IcC52hJDYsY', // (recommended) uncompressed public key P-256 encoded in Base64-URL
          'privateKey' => 'your web-certificate private-key', // (recommended) in fact the secret multiplier of the private key encoded in Base64-URL
      ],
  ];

  $webPush = new WebPush($auth);

  $subsrciptionData = json_decode($subsrciptionJson,true);


  // webpush 6.0
  $webPush->sendOneNotification(
    Subscription::create($subsrciptionData),
    json_encode($payloadData) // optional (defaults null)
  );
  

结论

希望这篇文章能帮助你了解如何将 Web 应用转化为 PWA。你可以在这里查看本文的源代码,并在这里体验演示。我还在示例代码的帮助下,通过从后端发送推送通知来测试推送功能。