aboutsummaryrefslogtreecommitdiff
path: root/files/zh-cn/web/progressive_web_apps/re-engageable_notifications_push/index.html
blob: 7a1ae4f840dc40e5babf2f1b6b9d54d0ea6df63e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
---
title: 通过通知推送让 PWA 可重用
slug: Web/Progressive_web_apps/Re-engageable_Notifications_Push
translation_of: Web/Progressive_web_apps/Re-engageable_Notifications_Push
---
<div>{{PreviousMenuNext("Web/Apps/Progressive/Installable_PWAs", "Web/Apps/Progressive/Loading", "Web/Apps/Progressive")}}</div>

<p class="summary">以本地缓存实现离线应用是一个强大的特性。允许用户在主屏幕上安装 Web 应用程序则显得更了不起。但除了单纯依赖用户手动打开应用之外,我们还可以走得更远,利用通知和消息推送提高用户参与度,并且随时提供新的内容。</p>

<h2 id="两个_API,一个目标">两个 API,一个目标</h2>

<p><a href="/en-US/docs/Web/API/Push_API">推送 API</a><a href="/en-US/docs/Web/API/Notifications_API">通知 API</a>是两个相互独立的 API,但在提高用户参与度这件事上,它们可以配合得很好。推送 API 可以用来从服务端推送新的内容而无需客户端介入,它是由应用的 Service Worker 来实现的;通知功能则可以通过 Service Worker 来向用户展示一些新信息,或者至少提醒用户应用已经更新了某些功能。</p>

<p>跟 Service Worker 一样,这些工作是在浏览器外部实现的,所以即使应用被隐藏到后台甚至被关闭了,我们仍然能够推送更新或者通知给用户。</p>

<h2 id="通知">通知</h2>

<p>让我们先从通知 API 开始,它能够脱离推送 API 单独工作,但两者结合起来会非常的有用,我们先看看它单独能做什么。</p>

<h3 id="请求授权">请求授权</h3>

<p>为了能够显示通知,我们需要先请求用户的授权。最佳的实践经验告诉我们,不应该直接请求授权,而应该引导用户点击一个按钮,然后才弹出授权窗口:</p>

<pre class="brush: js">var button = document.getElementById("notifications");
button.addEventListener('click', function(e) {
    Notification.requestPermission().then(function(result) {
        if(result === 'granted') {
            randomNotification();
        }
    });
});</pre>

<p>下面的图片展示了如何通过系统的通知服务显示一个授权窗口:</p>

<p><img alt="js13kPWA 的授权窗口。" src="https://mdn.mozillademos.org/files/15930/js13kpwa-notification.png" style="display: block; height: 640px; margin: 0px auto; width: 360px;"></p>

<p>当用户确定接收通知,应用就可以获得推送通知的功能。用户的授权的结果有三种,default(默认)、granted(允许)或者 denied(拒绝),当用户没有做出选择的时候,授权结果会返回 default,另外两种结果分别在用户选择了允许或者拒绝的时候返回。</p>

<p>一旦用户选择授权,这个授权结果对通知 API 和推送 API 两者都有效。</p>

<h3 id="创建一个通知">创建一个通知</h3>

<p>我们的示例应用通过可用的数据来创建通知。随机选择一个游戏数据来填充通知的主体:用游戏的名称来作为通知的标题,通知的主体会提及游戏的作者,用游戏的图片来作为通知的图标:</p>

<pre class="brush: js">function randomNotification() {
    var randomItem = Math.floor(Math.random()*games.length);
    var notifTitle = games[randomItem].name;
    var notifBody = 'Created by '+games[randomItem].author+'.';
    var notifImg = 'data/img/'+games[randomItem].slug+'.jpg';
    var options = {
        body: notifBody,
        icon: notifImg
    }
    var notif = new Notification(notifTitle, options);
    setTimeout(randomNotification, 30000);
}</pre>

<p>上述代码每隔三十秒会创建一个通知,直到用户觉得通知有点烦人并手动关闭掉它为止。一个真正的 App,对通知的频率必须进行严格的控制,通知内容也要更加有用才行。通知 API 的优势在于它使用了操作系统的通知功能,这意味着即使用户当前并没有在使用我们的应用,我们的通知也能够展示,它看起来跟一个原生应用发出的通知差不多。</p>

<h2 id="推送">推送</h2>

<p>推送比通知要复杂一些,我们需要从服务端订阅一个服务,之后服务端会推送数据到客户端应用。应用的 Service Worker 将会接收到从服务端推送的数据,这些数据可以用来做通知推送,或者实现其他的需求。</p>

<p>这个技术还处在非常初级的阶段,一些可用示例使用了谷歌云的消息推送平台,但它们正在被重写以支持 <a href="https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/">VAPID</a> (Voluntary Application Identification),它能为你的应用提供一层额外的安全防护。你可以看一下 <a href="https://serviceworke.rs/push-payload.html">Service Workers Cookbook 里的一些例子</a>,尝试用 <a href="https://firebase.google.com/">Firebase</a> 配置一个消息推送服务,或者构建自己的推送服务(例如使用 Node.js)。</p>

<p>之前提到,为了接收到推送的消息,你需要有一个 Service Worker,这部分的基础知识我们已经在文章<a href="https://developer.mozilla.org/zh-CN/docs/Web/Apps/Progressive/Offline_Service_workers" rel="nofollow">通过 Service workers 让 PWA 离线工作</a>里面解释过。在 Service Worker 内部,存在一个消息推送服务订阅机制。</p>

<pre class="brush: js">registration.pushManager.getSubscription() .then( /* ... */ );</pre>

<p>一旦用户订阅了通知服务,他们就能接收到服务器推送的通知。</p>

<p>从服务端的角度来看,出于安全的目的,这整个过程必须使用非对称加密技术进行保护:允许任何人用你的应用发送未加密的消息就大有问题了。你可以通过阅读<a href="https://jrconlin.github.io/WebPushDataTestPage/">Web 推送数据加密测试页</a>里面的详细信息来保护你的服务器。当用户订阅服务时,服务器会储存所有接收到的信息,以便在后续需要的时候能将信息推送出去。</p>

<p>为了能够接收到推送的消息,我们需要在 Service Worker 文件里面监听 {{event("push")}} 事件:</p>

<pre class="brush: js">self.addEventListener('push', function(e) { /* ... */ });</pre>

<p>这些数据被接收后,将会以通知的方式立刻展现给用户,例如提醒用户一些待办事项,或者让用户知道 App 有了新内容。</p>

<h3 id="推送例子">推送例子</h3>

<p>推送需要服务端,但因为应用托管在 GitHub 上,而 GitHub 只提供静态页面托管,我们没办法将服务端的例子包含进 js13kPWA 这个应用里面。<a href="https://serviceworke.rs/">Service Worker Cookbook</a> 这个页面上有详细的说明,demo 请看 <a href="https://serviceworke.rs/push-payload.html">Push Payload Demo</a></p>

<p>这个 demo 包含三个文件:</p>

<ul>
 <li><a href="https://github.com/mozilla/serviceworker-cookbook/blob/master/push-payload/index.js">index.js</a>, 包含我们应用的源代码</li>
 <li><a href="https://github.com/mozilla/serviceworker-cookbook/blob/master/push-payload/server.js">server.js</a>,包含服务端的源代码 (用 Node.js 编写)</li>
 <li><a href="https://github.com/mozilla/serviceworker-cookbook/blob/master/push-payload/service-worker.js">service-worker.js</a>, 包含 Service Worker 的具体代码。</li>
</ul>

<p>让我们一起看看这些代码:</p>

<h4 id="index.js">index.js</h4>

<p>index.js 的开头注册了 Service Worker:</p>

<pre class="brush: js">navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
  return registration.pushManager.getSubscription()
  .then(async function(subscription) {
      // 注册部分
  });
})
.then(function(subscription) {
    // 订阅部分
});</pre>

<p>这个 Service Worker 比我们在 <a href="https://mdn.github.io/pwa-examples/js13kpwa/">js13kPWA demo</a> 看到的要稍微复杂一些。在这部分代码里,Service Worker 注册完成之后,我们使用 registration 对象来发起订阅,然后使用 subscription 对象来完成整个流程。</p>

<p>注册部分的代码看起来大致是这个样子:</p>

<pre class="brush: js">if(subscription) {
    return subscription;
}</pre>

<p>如果用户已经完成订阅,我们直接返回 subscription 对象并且跳转到订阅部分。如果没有,我们会初始化一个新的 subscription 对象:</p>

<pre class="brush: js">const response = await fetch('./vapidPublicKey');
const vapidPublicKey = await response.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);</pre>

<p>App 会从服务器请求一个公钥并且把响应结果转化成文本,最后它还需要转化成一个 Uint8Array(为了兼容 Chrome)。如果你想学习更多关于 VAPID 的内容可以阅读 <a href="https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/">Sending VAPID identified WebPush Notifications via Mozilla’s Push Service</a> 这篇文章。</p>

<p>App 现在可以使用 {{domxref("PushManager")}} 来订阅新用户。这个方法支持传递两个参数:第一个是 <code>userVisibleOnly: true</code>,意思是发送给用户的所有通知对他们都是可见的,第二个是 <code>applicationServerKey</code>,这个参数包含我们之前从服务端取得并转化的 VAPID key。</p>

<pre class="brush: js">return registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: convertedVapidKey
});</pre>

<p>现在我们把注意力转移到订阅部分:App 首先使用 fetch 将 subscription 以 JSON 的方式发送给服务器。</p>

<pre class="brush: js">fetch('./register', {
    method: 'post',
    headers: {
        'Content-type': 'application/json'
    },
    body: JSON.stringify({
        subscription: subscription
    }),
});</pre>

<p>接着给“注册”按钮绑定了一个函数:</p>

<pre class="brush: js">document.getElementById('doIt').onclick = function() {
    const payload = document.getElementById('notification-payload').value;
    const delay = document.getElementById('notification-delay').value;
    const ttl = document.getElementById('notification-ttl').value;

    fetch('./sendNotification', {
        method: 'post',
        headers: {
            'Content-type': 'application/json'
        },
        body: JSON.stringify({
            subscription: subscription,
            payload: payload,
            delay: delay,
            ttl: ttl,
        }),
    });
};</pre>

<p>当按钮被点击的时候,<code>fetch</code> 会请求服务器根据给定的参数发送通知,<code>payload</code> 参数包含通知里面要显示的文本,<code>delay</code> 参数定义了通知将会延迟展示的秒数,<code>ttl</code> 是 time-to-live 的缩写,用来设置通知在服务器上的存活时间,依然是以秒为单位。</p>

<p>接着,我们进入下一个 JavaScript 文件。</p>

<h4 id="server.js">server.js</h4>

<p>服务端的代码是用 Node.js 编写的,它需要被托管在合适的地方。这部分内容已经够单独再写一篇文章了。所以这里我们只提供一个简单的概览。</p>

<p><a href="https://www.npmjs.com/package/web-push">web-push</a> 模块被用来配置 VAPID 密钥,如果没有的话,还可以生成一个。</p>

<pre>const webPush = require('web-push');

if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) {
  console.log("You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY "+
    "environment variables. You can use the following ones:");
  console.log(webPush.generateVAPIDKeys());
  return;
}

webPush.setVapidDetails(
  'https://serviceworke.rs/',
  process.env.VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
);
</pre>

<p>接着,一个模块定义并导出了所有应用需要处理的路由:获取 VAPID 公钥、注册、发送通知等等。你可以看到来自 <code>index.js</code> 的 <code>payload</code>, <code>delay</code><code>ttl</code> 变量在这里被用到了。</p>

<pre>module.exports = function(app, route) {
  app.get(route + 'vapidPublicKey', function(req, res) {
    res.send(process.env.VAPID_PUBLIC_KEY);
  });

  app.post(route + 'register', function(req, res) {

    res.sendStatus(201);
  });

  app.post(route + 'sendNotification', function(req, res) {
    const subscription = req.body.subscription;
    const payload = req.body.payload;
    const options = {
      TTL: req.body.ttl
    };

    setTimeout(function() {
      webPush.sendNotification(subscription, payload, options)
      .then(function() {
        res.sendStatus(201);
      })
      .catch(function(error) {
        console.log(error);
        res.sendStatus(500);
      });
    }, req.body.delay * 1000);
  });
};</pre>

<h4 id="service-worker.js">service-worker.js</h4>

<p>最后我们要看的文件是 Service Worker。</p>

<pre>self.addEventListener('push', function(event) {
    const payload = event.data ? event.data.text() : 'no payload';
    event.waitUntil(
        self.registration.showNotification('ServiceWorker Cookbook', {
            body: payload,
        })
    );
});</pre>

<p>它做的就只是监听 {{event("push")}} 事件,创建 payload 变量,这个变量包含了来自 event.data 的文本(如果 data 是空的,就设置成 "no payload" 字符串),然后一直等到通知推送给用户为止。</p>

<p>如果你想知道它们具体是怎么处理的,请随意探索 <a href="https://serviceworke.rs/">Service Worker Cookbook</a> 里面剩下的例子。<a href="https://github.com/mozilla/serviceworker-cookbook/">GitHub 上面提供了完整的源代码</a>,也有大量的示例展示了各种用法,包括 Web 推送、缓存策略、性能、离线运行等等。</p>

<p>{{PreviousMenuNext("Web/Apps/Progressive/Installable_PWAs", "Web/Apps/Progressive/Loading", "Web/Apps/Progressive")}}</p>

<div>{{QuickLinksWithSubpages("/en-US/docs/Web/Progressive_web_apps/")}}</div>