将你的网站转变为 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 并获得用户授权。将以下代码粘贴到 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 账户,然后点击 “开始”。
- 创建一个新项目并命名,然后点击“继续”。我将使用名称 techblik.com 来创建它。
- 在下一步中,默认启用 Google Analytics。你可以选择不启用(我们现在不需要),然后点击 “继续”。如有需要,你可以在稍后在 Firebase 控制台中启用它。
- 项目创建后,将会显示如下界面。
然后,进入项目设置,点击“云消息传递”,并生成密钥。
通过以上步骤,你将获得 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。你可以在这里查看本文的源代码,并在这里体验演示。我还在示例代码的帮助下,通过从后端发送推送通知来测试推送功能。