aboutsummaryrefslogtreecommitdiff
path: root/files/zh-cn/conflicting/web/api/push_api/index.html
blob: 6f6f75ce4fb586cb75d0f80c23b16918d704fb55 (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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
---
title: Using the Push API
slug: conflicting/Web/API/Push_API
tags:
  - Push
  - Push API
  - Service Workers
  - Using the Push API
translation_of: Web/API/Push_API
translation_of_original: Web/API/Push_API/Using_the_Push_API
original_slug: Web/API/Push_API/Using_the_Push_API
---
<p class="summary"><span>W3C <a href="/en-US/docs/Web/API/Push_API">Push API</a> </span>为开发人员在Web应用程序中提供了一些令人兴奋的新功能:本文提供了一个简单的演示,以获取Push通知的设置和运行。</p>

<p>在任何时候——不论这一应用是否处于激活状态——从服务器向客户端推送消息或者通知的能力,是一种原生应用已经享受了一段时间的能力。现在 Web 应用也拥有了这一能力。桌面系统上的 Firefox 43+ 和 Chrome 42+ 已经支持 Push 的大部分功能,移动平台也很可能在不久的将来提供支持。 {{domxref("PushMessageData")}} 当前只在 Firefox Nightly (44+) 中提供实验性的支持,并且这一实现也可能会变更。</p>

<div class="note">
<p><strong>Note</strong>: Firefox OS 的早期版本使用了这一 API 的一个 proprietary 版本,叫做 <a href="/en-US/docs/Web/API/Simple_Push_API">Simple Push</a> ,现在已经被 Push API 标准废弃。</p>
</div>

<h2 id="Demo_the_basis_of_a_simple_chat_server_app">Demo: the basis of a simple chat server app</h2>

<p>我们创建的这一 demo 是一个简单的聊天应用。它提供了一个表单,用来输入聊天内容,还有一个按钮,用来订阅(subscribe)推送的消息。按下按钮后,你将订阅这一消息推送服务,服务器会记录你的信息,同时当前所有的订阅者会收到一个推送消息,告诉他们有人订阅。</p>

<p>此时,新订阅者的名字会出现在订阅者列表上,同时界面上会出现一个文本域和一个提交按钮,允许订阅者发送消息。</p>

<p><img alt="" src="https://mdn.mozillademos.org/files/11823/push-api-demo.png" style="border: 1px solid black; display: block; height: 406px; margin: 0px auto; width: 705px;"></p>

<p>要运行这一 demo,请参阅 <a href="https://github.com/chrisdavidmills/push-api-demo">push-api-demo README</a>。请注意,想要在 Chrome 中使用这一应用并且以一个更合理的方式运行,服务器端还需要大量的工作。然而,推送的细节解释起来特别麻烦,我们先概览这个推送接口是怎么运作的,然后再回来详细了解。</p>

<h2 id="Technology_overview">Technology overview</h2>

<p>这一部分提供了这一例子中用到的技术的概览。</p>

<p>Web Push 消息是 <a href="/en-US/docs/Web/API/Service_Worker_API">service workers</a> 技术族的一部分;特别的,一个service worker想要接收消息,就必须在一个页面上被激活。 在 service worker 接收到推送的消息后,你可以决定如何显示这一消息。你可以:</p>

<ul>
 <li>发送一个 <a href="/en-US/docs/Web/API/Notifications_API">Web notification</a> ,弹出一个系统通知提醒用户。这一操作需要发送推送消息的权限。</li>
 <li>通过 {{domxref("MessageChannel")}} 将消息送回主页面。</li>
</ul>

<p>通常这两者可以结合使用,下面的 demo 显示了两者的特点。</p>

<div class="note">
<p><span style="font-size: 14px;"><strong>注:</strong>你需要在服务器端部署某种形式的代码来处理endpoint数据的加密和发送推送消息的请求。</span> 在我们的Demo里,我们把那些代码放进了一个快速、劣质的服务器代码(a quick-and-dirty server )里,部署在 <a href="https://nodejs.org/">NodeJS</a> 上。</p>
</div>

<p>service worker 需要订阅推送消息服务。在订阅服务时,每一个会话会有一个独立的端点(endpoint)。订阅对象的属性({{domxref("PushSubscription.endpoint")}}) 即为端点值。将端点发送给服务器后,服务器用这一值来发送消息给会话的激活的 service worker。不同浏览器需要用不同的推送消息服务器。</p>

<h3 id="加密(Encryption)">加密(Encryption)</h3>

<div class="note">
<p><strong>Note</strong>: For an interactive walkthrough, try JR Conlin's <a href="https://jrconlin.github.io/WebPushDataTestPage/">Web Push Data Encryption Test Page</a>.</p>
</div>

<p>在通过推送消息发送数据时,数据需要进行加密。数据加密需要通过 {{domxref("PushSubscription.getKey()")}} 方法产生的一个公钥。这一方法在服务器端运行,通过复杂的密码学机制来生成公钥,详情可参阅 <a href="https://tools.ietf.org/html/draft-ietf-webpush-encryption-01">Message Encryption for Web Push</a> 。以后可能会有更多用于处理推送消息加密的库出现,在这一 demo 中,我们使用 Marco Castelluccio's NodeJS <a href="https://github.com/marco-c/web-push">web-push library</a>.</p>

<div class="note">
<p><strong>Note</strong>: There is also another library to handle the encryption with a Node and Python version available, see <a href="https://github.com/martinthomson/encrypted-content-encoding">encrypted-content-encoding</a>.</p>
</div>

<h3 id="Push_workflow_summary">Push workflow summary</h3>

<p>这里我们总结一下推送消息的实现。在之后的章节中你可以找到这一 demo 代码的更多细节。</p>

<ol>
 <li>请求 web 通知及你所使用的其他功能的权限。</li>
 <li>调用 {{domxref("ServiceWorkerContainer.register()")}} ,注册一个 service worker。</li>
 <li>使用 {{domxref("PushManager.subscribe()")}} 订阅推送消息。</li>
 <li>取得与订阅相关联的 endpoint ({{domxref("PushSubscription.endpoint")}}),并且生成一个客户公钥({{domxref("PushSubscription.getKey()")}}) 。注意 <code>getKey()</code> 是试验性的,只在 Firefox 有效。</li>
 <li>将详细信息发送给服务器,服务器可以用这些信息来发送推送消息。这一 demo 使用 {{domxref("XMLHttpRequest")}} ,但你也可以使用 <a href="/en-US/docs/Web/API/Fetch_API">Fetch</a></li>
 <li>如果你使用 <a href="/en-US/docs/Web/API/Channel_Messaging_API">Channel Messaging API</a> 来和 service worker 通信,则创建一个新的 message channel ({{domxref("MessageChannel.MessageChannel()")}}) ,并且在 service worker 调用 {{domxref("Worker.postMessage()")}} ,将 <code>port2</code> 发送给 service worker ,以建立 communication channel 。你应该设置一个 listener 来响应从 service worker 发来的消息。</li>
 <li>在服务器端,存储端点以及其他在发送推送消息给订阅者时需要的信息(我们使用一个简单的文本文件,但你可以使用数据库,或者其他你喜欢的方式)。在生产环境中,请保护好这些信息,以防恶意的攻击者用这些信息给订阅者推送垃圾消息。</li>
 <li>要发送一个推送消息,你需要向端点 URL 发送一个 HTTP <code>POST</code> 。这一请求需要包括一个 TTL 头,用来规定用户离线时消息队列的最大长度。要在请求中包括数据,你需要使用客户公钥进行加密。在我们的 demo 中,我们使用 <a href="https://github.com/marco-c/web-push">web-push</a> 模块来处理困难的部分。</li>
 <li>在你的 service worker 中,设置一个 <code>push</code> 事件句柄来响应接收到的推送消息。
  <ol>
   <li>如果你想要将一个信道消息发送回主 context(看第6步),你需要先取得之前发送给 service worker 的  <code>port2</code> 的引用 ({{domxref("MessagePort")}}) 。这个可以通过传给 <code>onmessage</code> handler ({{domxref("ServiceWorkerGlobalScope.onmessage")}}) 的{{domxref("MessageEvent")}} 对象取得。 具体地说,是 <code>ports</code> 属性的索引 0 。 之后你可以用 {{domxref("MessagePort.postMessage()")}} 来向 <code>port1</code> 发送消息 。</li>
   <li>如果你想要使用系统通知,可以调用 {{domxref("ServiceWorkerRegistration.showNotification()")}} 。注意,在我们的代码中,我们将其运行在一个 {{domxref("ExtendableEvent.waitUntil()")}} 方法中——这样做将事件的 生命周期(lifetime)扩展到了通知被处理后,使得我们可以确认事情像我们期望的那样进行。<span id="cke_bm_307E" class="hidden"> </span></li>
  </ol>
 </li>
</ol>

<h2 id="Building_up_the_demo">Building up the demo</h2>

<p>让我们浏览一下 demo 的代码,理解一下它是如何工作的。</p>

<h3 id="The_HTML_and_CSS">The HTML and CSS</h3>

<p>这个 demo 的 HTML 和 CSS 没有什么需要特别留意的地方。初始化时,HTML包含一个简单的表单、一个按钮和两个列表。按钮用来订阅,两个列表分别显示订阅者和聊天消息。订阅之后,会出现用来输入聊天消息的控件。</p>

<p>为了对不干扰 Push API 的理解,CSS被设计得非常简单。</p>

<h3 id="The_main_JavaScript_file">The main JavaScript file</h3>

<p>JavaScript 明显更加重要。让我们看看主 JS 文件。</p>

<h4 id="Variables_and_initial_setup">Variables and initial setup</h4>

<p>开始时,我们声明一些需要使用的变量:</p>

<pre class="brush: js">var isPushEnabled = false;
var useNotifications = false;

var subBtn = document.querySelector('.subscribe');
var sendBtn;
var sendInput;

var controlsBlock = document.querySelector('.controls');
var subscribersList = document.querySelector('.subscribers ul');
var messagesList = document.querySelector('.messages ul');

var nameForm = document.querySelector('#form');
var nameInput = document.querySelector('#name-input');
nameForm.onsubmit = function(e) {
  e.preventDefault()
};
nameInput.value = 'Bob';</pre>

<p>首先,是两个布尔变量,一个用来记录 Push 是否被订阅,一个用来记录是否有通知的权限。</p>

<p>其次,是订阅/取消订阅 {{htmlelement("button")}} 的引用,以及发送消息按钮的引用和输入的引用(订阅成功之后按钮和输入才会创建)。</p>

<p>接下来,是页面上的三个{{htmlelement("div")}} 元素的引用,在需要插入元素时会用到(比如创建 <em>Send Chat Message</em> 按钮,或者 <em>Messages </em>列表中增加聊天消息时)。</p>

<p>最后,是 name selection 表单和 {{htmlelement("input")}} 元素的引用。我们给 input 一个默认值,并且使用 <code><a href="/en-US/docs/Web/API/Event/preventDefault">preventDefault()</a></code> 方法,让按下回车时不会自动提交表单。</p>

<p>之后,我们通过 {{domxref("Notification.requestPermission","requestPermission()")}} 请求发送web通知的权限:</p>

<pre class="brush: js">Notification.requestPermission();</pre>

<p><code><a href="/en-US/docs/Web/API/GlobalEventHandlers/onload">onload</a></code> 时我们运行一段代码,让应用开始初次加载时的初始化过程。首先我们给 subscribe/unsubscribe 按钮添加 click event listener ,让这一按钮在已经订阅(<code>isPushEnabled为真)</code>时执行 <code>unsubscribe()</code> 函数,否则执行 <code>subscribe()</code></p>

<pre class="brush: js">window.addEventListener('load', function() {
  subBtn.addEventListener('click', function() {
    if (isPushEnabled) {
      unsubscribe();
    } else {
      subscribe();
    }
  });</pre>

<p>之后,我们检查 service workers 是否被支持。如果支持,则用 {{domxref("ServiceWorkerContainer.register()")}} 注册一个 service worker 并且运行 <code>initialiseState()</code> 函数。若不支持,则向控制台输出一条错误信息。</p>

<pre class="brush: js">  // Check that service workers are supported, if so, progressively
  // enhance and add push messaging support, otherwise continue without it.
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('sw.js').then(function(reg) {
      if(reg.installing) {
        console.log('Service worker installing');
      } else if(reg.waiting) {
        console.log('Service worker installed');
      } else if(reg.active) {
        console.log('Service worker active');
      }

      initialiseState(reg);
    });
  } else {
    console.log('Service workers aren\'t supported in this browser.');
  }
});
</pre>

<p>接下来是 <code>initialiseState()</code> 函数。查看 <a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/main.js"><code>initialiseState()</code> source on Github</a> 可获得有注释的完整源码(简洁起见,此处省略)。</p>

<p><code>initialiseState()</code> 首先检查 service workers 是否支持 notifications ,如果支持则将 <code>useNotifications</code> 变量设为真。之后检查用户是否允许 said notifications , push messages 是否支持,并分别进行设置。</p>

<p>最后,使用 {{domxref("ServiceWorkerContainer.ready()")}} 来检测 service worker 是否被激活并开始运行,会返回一个Promise对象。当这一 promise 对象 resolve 时,我们访问 {{domxref("ServiceWorkerRegistration.pushManager")}} 属性,得到一个 {{domxref("PushManager")}} 对象,再调用该对象的 {{domxref("PushManager.getSubscription()")}}方法,最终获得用来推送消息的订阅对象。当第二个 promise 对象 resolve 时,我们启用 subscribe/unsubscribe 按钮(<code>subBtn.disabled = false;)</code>,并确认订阅对象。</p>

<p>这样做了之后,订阅的准备工作已经完成了。即使这一应用并没有在浏览器中打开, service worker 也依然可能在后台处于激活状态。如果我们已经订阅,则对UI进行更新,修改按钮的标签,之后将 <code>isPushEnabled 设为真,通过 </code>{{domxref("PushSubscription.endpoint")}} 取得订阅的端点,通过 {{domxref("PushSubscription.getKey()")}} 生成一个公钥,调用<code> updateStatus()</code> 方法与服务器进行通信。</p>

<p>此外,我们通过 {{domxref("MessageChannel.MessageChannel()")}} 得到一个新的 {{domxref("MessageChannel")}} 对象,并通过 {{domxref("ServiceworkerRegistration.active")}} 得到一个激活的 service worker 的引用,然后通过 {{domxref("Worker.postMessage()")}} 在主浏览器 context 和 service worker 间建立起一个信道。浏览器 context 接收 {{domxref("MessageChannel.port1")}} 中的消息。当有消息到来时,我们使用 <code>handleChannelMessage()</code> 方法来决定如何处理数据(参阅{{anch("Handling channel messages sent from the service worker")}})。</p>

<h4 id="订阅和取消订阅(Subscribing_and_unsubscribing)">订阅和取消订阅(Subscribing and unsubscribing)</h4>

<p>现在把我们的注意力转到 <code>subscribe()</code> 和 <code>unsubscribe()</code> 函数上,它们用来订阅或取消订阅 来自服务器的通知。</p>

<p>在订阅的时候,我们使用 {{domxref("ServiceWorkerContainer.ready()")}}方法再一次确认service worker处于激活状态并且可以使用了。当 promise 成功执行,我们用{{domxref("PushManager.subscribe()")}}方法订阅服务。如果订阅成功,我们会得到一个 {{domxref("PushSubscription")}} 对象,它携带了endpoint信息 ,并且可以产生(generate)一个公钥的方法 (再多说一点,{{domxref("PushSubscription.endpoint")}}属性和{{domxref("PushSubscription.getKey()")}}方法),我们要把这两个信息传递给<code>updateStatus()</code> 函数,同时还要传递第三个信息——更新状态的类型(订阅还是不订阅),让它能够把这些必要的细节传递给服务器。</p>

<p>我们也需要更新我们应用的状态 (设置 <code>isPushEnabled</code><code>true</code>) 和 UI (激活<strong>订阅</strong>或者不订阅的按钮,同时改变标签的显示状态,让用户下一次点击按钮的时候变成<strong>不订阅</strong>的状态或者订阅的状态。)</p>

<p><code><font face="Open Sans, Arial, sans-serif">不订阅 </font>unsubscribe()</code> 函数在结构上和订阅函数相识,然而基本上它们做的是完全相反的事; 最值得注意的不同是得到当前订阅对象是使用{{domxref("PushManager.getSubscription()")}}方法,而且使用{{domxref("PushSubscription.unsubscribe()")}}方法获得的promise对象。</p>

<p>在两个函数中也提供了适当的错误处理函数。</p>

<p><code><font face="Open Sans, Arial, sans-serif">为了节省时间,我们只在下面展示</font>subscribe()的代码;</code>查看全部请点击 <a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/main.js">subscribe/unsubscribe code on Github</a>.</p>

<pre class="brush: js">function subscribe() {
  // Disable the button so it can't be changed while
  // we process the permission request

  subBtn.disabled = true;

  navigator.serviceWorker.ready.then(function(reg) {
    reg.pushManager.subscribe({userVisibleOnly: true})
      .then(function(subscription) {
        // The subscription was successful
        isPushEnabled = true;
        subBtn.textContent = 'Unsubscribe from Push Messaging';
        subBtn.disabled = false;

        // Update status to subscribe current user on server, and to let
        // other users know this user has subscribed
        var endpoint = subscription.endpoint;
        var key = subscription.getKey('p256dh');
        updateStatus(endpoint,key,'subscribe');
      })
      .catch(function(e) {
        if (Notification.permission === 'denied') {
          // The user denied the notification permission which
          // means we failed to subscribe and the user will need
          // to manually change the notification permission to
          // subscribe to push messages
          console.log('Permission for Notifications was denied');

        } else {
          // A problem occurred with the subscription, this can
          // often be down to an issue or lack of the gcm_sender_id
          // and / or gcm_user_visible_only
          console.log('Unable to subscribe to push.', e);
          subBtn.disabled = false;
          subBtn.textContent = 'Subscribe to Push Messaging';
        }
      });
  });
}</pre>

<h4 id="更新应用和服务器的状态">更新应用和服务器的状态</h4>

<p><code><font face="Open Sans, Arial, sans-serif">接下来的一个主要的JavaScript函数就是</font>updateStatus(),当订阅和取消订阅的时候,它负责更新UI中与服务器沟通的信息并发送状态更新的请求给服务器。</code></p>

<p>这个函数做了三件事当中的哪一件事,取决于下面赋值给statusType的类型:</p>

<ul>
 <li><code>subscribe</code>: 一个按钮和和一个聊天信息的input输入框被创建后,就被插入到UI界面里面,然后通过XHR请求发送了一个包含状态信息的字面量对象给服务器,包含了statusType(subscribe)、订阅者的名字( username of the subscriber)、订阅终端(subscription endpoint)和客户端公钥(client public key)。</li>
 <li><code>unsubscribe</code>: 这个和订阅基本上是相反的——聊天的按钮和输入框被移除,同时又一个字面量对象被发送给服务器,告诉它取消订阅。</li>
 <li><code>init</code>: 当应用被第一次载入或者安装的时候会运行——它创建与服务器沟通的UI,并且发送一个对象告诉服务器是哪一个用户初始化(重新载入)了。</li>
</ul>

<p>再多说一句,为了简介这里不会展示全部的代码。检查全部代码点击: <a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/main.js">full <code>updateStatus()</code> code on Github</a>.</p>

<h4 id="处理在service_worker中发送过来的channel_message">处理在service worker中发送过来的channel message</h4>

<p>正如刚才我们提到的,当我们接收到从service worker发送的<a href="/en-US/docs/Web/API/Channel_Messaging_API">channel message</a> 时,我们的 <code>handleChannelMessage()</code> 函数才会去执行它。 我们用{{domxref("channel.port1.onmessage")}}事件处理函数去处理{{event("message")}} event, :</p>

<pre class="brush: js">channel.port1.onmessage = function(e) {
  handleChannelMessage(e.data);
}</pre>

<p>这个函数会在service worker中发送信息给页面的时候在页面中执行(This occurs when the <a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/sw.js#L8">service worker sends a channel message over</a>)。</p>

<p> <code>handleChannelMessage()</code> 函数如下:</p>

<pre class="brush: js">function handleChannelMessage(data) {
  if(data.action === 'subscribe' || data.action === 'init') {
    var listItem = document.createElement('li');
    listItem.textContent = data.name;
    subscribersList.appendChild(listItem);
  } else if(data.action === 'unsubscribe') {
    for(i = 0; i &lt; subscribersList.children.length; i++) {
      if(subscribersList.children[i].textContent === data.name) {
        subscribersList.children[i].parentNode.removeChild(subscribersList.children[i]);
      }
    }
    nameInput.disabled = false;
  } else if(data.action === 'chatMsg') {
    var listItem = document.createElement('li');
    listItem.textContent = data.name + ": " + data.msg;
    messagesList.appendChild(listItem);
    sendInput.value = '';
  }
}</pre>

<p>这个函数会做什么取决于传入的action参数的赋值:</p>

<ul>
 <li><code>subscribe</code> or <code>init</code> (在启动和重启的时候,我们需要在这个示例中做同样的事情):一个{{htmlelement("li")}} 元素被创建,它的text content 被设置为 <code>data.name</code> (订阅者的名字),然后它被添加到订阅者的列表里(一个简单的 {{htmlelement("ul")}}元素里),所以这里是订阅者(再次)加入聊天的一个视觉反馈。</li>
 <li><code>unsubscribe</code>: 我们循环遍历订阅者的列表,查找谁的 text content 和<code>data.name</code> (取消订阅者的名字)相同,然后删除这个节点以提供一个视觉反馈表示有人取消订阅了。</li>
 <li><code>chatMsg</code>: 和第一个表现的形式类似, 一个 {{htmlelement("li")}}被创建,它的text content设置为<code>data.name + ": " + data.msg</code> (例如: "Chris: This is my message"), 然后它被添加到每一个用户UI的聊天列表里。</li>
</ul>

<div class="note">
<p><span style="font-size: 14px;"><strong>注: </strong></span> 我们需要在更新DOM之前传递需要的数据给主页面(main context), 因为service worker不能操作DOM。你在使用前一定要知道service worker的一些限制。阅读 <a href="/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers">Using Service Workers</a> 获取更多细节.</p>
</div>

<h4 id="发送聊天信息">发送聊天信息</h4>

<p>当‘<em>Send Chat Message‘ </em>按钮被点击后,相关联的文本域的内容就作为聊天内容被发送出去。这个由 <a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/main.js"><code>sendChatMessage()</code> </a>函数处理(再多说一句,为了简洁就不展示了). 这个和 <code>updateStatus()</code> 的不同之处也是差不多的。 (查看 {{anch("Updating the status in the app and server")}}) — 我们获得 endpoint 和 public key 是来自一个 {{domxref("PushSubscription")}} 对象, 这个对象又是来自于 {{domxref("ServiceWorkerContainer.ready()")}} 方法和{{domxref("PushManager.subscribe()")}}方法。它们(endpoint、public key)被传递进一个字面量对象里面( in a message object),同时含有订阅用户的名字,聊天信息,和chatMsg的statusType,然后通过{{domxref("XMLHttpRequest")}}对象发送出去的,。</p>

<h3 id="服务端(The_Server)">服务端(The Server)</h3>

<p>正如我们上面提到的,我们需要一个服务端的容器去存储订阅者的信息,并且还要在状态更新时发送推送消息给客户端。 我们已经用一种hack的方式把一些需要的东西放到了一个快速-劣质(quick-and-dirty)的<a href="http://nodejs.org/">NodeJS</a> 服务端上(<code><a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/server.js">server.js</a></code>),用于处理来自客户端JavaScript的异步请求。</p>

<p>它用一个文本文件 (<code><a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/endpoint.txt">endpoint.txt</a></code>)去存储订阅者的信息;这个文件一开始是空的(empty)。 有四种不同类型的请求,分别由被传输过来的对象中的statusType决定;这些在客户端通俗易懂的的状态类型同样也适用于服务端,因为服务端也有相同的状态。下面是这四个个状态在服务端代表的各自的含义。</p>

<ul>
 <li><code>subscribe</code>: 服务器将会添加订阅者的信息到存储的容器里 (<code><a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/endpoint.txt">endpoint.txt</a></code>),包括endpoint等,然后会推送给所有已经订阅了的订阅者一条消息,告诉每一个订阅者有新的订阅者加入了聊天。</li>
 <li><code>unsubscribe</code>: 服务器在存储订阅信息的容器里找到发送该类型请求的订阅者的详情,并移除这个订阅者的信息,然后推送一条消息给所有依然订阅的用户,告诉他们这个人取消订阅了。</li>
 <li><code>init</code>: 服务器从那个存储信息的文本中读取所有订阅者的信息,然后告诉他们有人初始化或者再次加入了这个聊天。</li>
 <li><code>chatMsg</code>: 推送一条订阅者想发送的信息给所有的订阅者;服务器从存储容器里读取现在订阅者的信息,然后服务器给每一个人推送一条包含聊天信息的消息。</li>
</ul>

<p>其他需要注意的东西:</p>

<ul>
 <li>我们使用的是Node.js的 <a href="https://nodejs.org/api/https.html">https 模块 </a> 去创建的服务器,因为出于安全考虑,service worker只允许工作在一个安全的连接里(https only)。这也就是为什么我们要在应用里包含了<code>.pfx</code> 安全证书,然后在Node代码里引用这个证书。</li>
 <li>当你发送一条没有数据的推送消息的时候,只需要把它用http的post请求发送给对应订阅者的endpoint的URL。然而,当推送消息里包含数据的时候,你需要加密它,这个加密过程往往很复杂。随着时间的推移,一些出现的库文件(libraries)帮你做了这部分的工作。在这个demo里面我们用了 Marco Castelluccio的NodeJS库文件(<a href="https://github.com/marco-c/web-push">web-push library</a>)。查阅这些源代码了解更多加密过程是怎么完成的 (查阅 <a href="https://tools.ietf.org/html/draft-ietf-webpush-encryption-01">Message Encryption for Web Push</a> 获取更多细节)。库文件让发送一条推送消息变得简单( The library <a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/server.js#L43-L46">makes sending a push message simple</a>)。</li>
</ul>

<h3 id="The_service_worker">The service worker</h3>

<p>现在让我们来看看服务工作者代码(sw.js),它响应由{{Event("push")}}事件表示的推送消息。 这些通过({{domxref("ServiceWorkerGlobalScope.onpush")}})事件处理程序在服务工作人员的范围内处理; 它的工作就是为每个收到的消息做出回应。 我们首先通过调用{{domxref("PushMessageData.json()")}}将收到的消息转换回对象。然后,通过查看这个对象的action属性,就能知道这个推送消息的类型:</p>

<ul>
 <li><code>subscribe</code> 或者 <code>unsubscribe</code>: 我们发送一个系统通知是通过 <code>fireNotification()</code> 函数,但同时,也通过{{domxref("MessageChannel")}}把这个消息发送回函数的上下文环境(main context),以便我们相应地更新订阅者列表(subscriber list) (查看 {{anch("Handling channel messages sent from the service worker")}} 了解详细).</li>
 <li><code>init</code> 或者 <code>chatMsg</code>: 我们只是发送一个消息回主要的上下文(main context)来处理 <code>init</code><code>chatMsg</code> 情况(这些不需要系统通知).</li>
</ul>

<pre class="brush: js">self.addEventListener('push', function(event) {
  var obj = event.data.json();

  if(obj.action === 'subscribe' || obj.action === 'unsubscribe') {
    fireNotification(obj, event);
    port.postMessage(obj);
  } else if(obj.action === 'init' || obj.action === 'chatMsg') {
    port.postMessage(obj);
  }
});</pre>

<p>下一步, 让我们看看 <code>fireNotification()</code> 函数的代码 (它真的太他妈简单了).</p>

<pre class="brush: js">function fireNotification(obj, event) {
  var title = 'Subscription change';
  var body = obj.name + ' has ' + obj.action + 'd.';
  var icon = 'push-icon.png';
  var tag = 'push';

  event.waitUntil(self.registration.showNotification(title, {
    body: body,
    icon: icon,
    tag: tag
  }));
}</pre>

<p>我们先在这里列出通知消息框所需要的资源:标题、主体内容、图标。然后我们通过 {{domxref("ServiceWorkerRegistration.showNotification()")}} 方法把这个通知发送出去,同时提供一个"push"给tag属性,表示这是一个推送消息,以便我们能够在全部的通知消息中找到(identify)这个推送消息。 当我们成功发送一条推送消息,它可能会在用户对应的电脑或者设备上展示一个系统通知对话框,在不同的设备上通知对话框的外观可能是不一样的(下面的这张图片展示的是Mac OSX系统上的通知对话框)。</p>

<p><img alt="" src="https://mdn.mozillademos.org/files/11855/subscribe-notification.png" style="display: block; height: 65px; margin: 0px auto; width: 326px;"></p>

<p>注意,我们做的这些都包裹在{{domxref("ExtendableEvent.waitUntil()")}} 方法里;这是为了让ServiceWorker在发送通知消息的过程中依然保持活动,知道消息发送完成。 <code>waitUntil()</code> 会延长service worker的生命周期至(这个周期里)所有活动都已经完成。</p>

<div class="note">
<p><strong>Note</strong>: Web notifications from service workers were introduced around Firefox version 42, but are likely to be removed again while the surrounding functionality (such as <code>Clients.openWindow()</code>) is properly implemented (see {{bug(1203324)}} for more details.)</p>
</div>

<h2 id="处理推送订阅过早失效(Handling_premature_subscription_expiration)">处理推送订阅过早失效(Handling premature subscription expiration)</h2>

<p>有时候,推送订阅会过早失效,而不会调用{{domxref("PushSubscription.unsubscribe()")}} 。例如,当服务器过载或长时间处于脱机状态时,可能会发生这种情况。这是高度依赖于服务器的,所以确切的行为很难预测。 在任何情况下,您都可以通过查看{{Event("pushsubscriptionchange")}} 事件来处理此问题,您可以通过提供{{domxref("ServiceWorkerGlobalScope.onpushsubscriptionchange")}} 事件处理程序来侦听此事件;这个事件只会在这种情况下才会被触发。</p>

<pre class="brush: js  language-js"><code class="language-js">self<span class="punctuation token">.</span><span class="function token">addEventListener<span class="punctuation token">(</span></span><span class="string token">'pushsubscriptionchange'</span><span class="punctuation token">,</span> <span class="keyword token">function</span><span class="punctuation token">(</span><span class="punctuation token">)</span> <span class="punctuation token">{</span>
 <span class="comment token"> // do something,一般来说都会在这里重新订阅,
  // 并通过XHR或者Fetch发送新的订阅信息回服务器</span><span class="comment token">
</span><span class="punctuation token">}</span><span class="punctuation token">)</span><span class="punctuation token">;</span></code></pre>

<p>注意,我们在demo里没有覆盖这种情况,因为订阅端点(subscription ending)作为一个简单的聊天服务器并不是什么难事。但是对于更复杂的示例,您可能需要重新订阅(resubscribe )用户。</p>

<h2 id="(让Chrome支持的额外步骤)Extra_steps_for_Chrome_support">(让Chrome支持的额外步骤)Extra steps for Chrome support</h2>

<p>为了让应用也能够在Chrome上面运行,我们需要一些额外的步骤,因为在Chrome上,现在需要依赖谷歌云消息推送(Google's Cloud Messaging) 服务才能正常工作。</p>

<h3 id="配置谷歌云消息推送(Setting_up_Google_Cloud_Messaging)">配置谷歌云消息推送(Setting up Google Cloud Messaging)</h3>

<p>按照以下步骤配置:</p>

<ol>
 <li>跳转到 <a href="https://console.developers.google.com">Google Developers Console</a>  然后创建一个新项目。</li>
 <li>进入你项目主页(例如:我们的示例是在 <code>https://console.developers.google.com/project/push-project-978</code>), 然后
  <ol>
   <li>在你的应用选项里打开<em> Google APIs </em></li>
   <li>在下一屏,在移动API那一部分下面点击“<em>Cloud Messaging for Android</em>” 。</li>
   <li>点击开启API按钮。</li>
  </ol>
 </li>
 <li>现在你需要记下你的项目编号和API key,因为待会儿你会用到他们。 它们在这些地方:
  <ol>
   <li><strong>项目编号</strong>: 点击左侧的主页;项目编号在你的项目主页上方很容易看见的位置。</li>
   <li><strong>API key</strong>: 点击左边菜单里的证书(<em>Credentials</em> );API key 能够在这个页面找到。</li>
  </ol>
 </li>
</ol>

<h3 id="manifest.json">manifest.json</h3>

<p>你需要在你的应用里包含Google应用风格的 <code>manifest.json</code> ,这里面的 <code>gcm_sender_id</code> 参数需要设置为你的项目编号。下面是一个简单的示例(<a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/manifest.json">manifest.json</a>):</p>

<pre class="brush: js">{
  "name": "Push Demo",
  "short_name": "Push Demo",
  "icons": [{
        "src": "push-icon.png",
        "sizes": "111x111",
        "type": "image/png"
      }],
  "start_url": "/index.html",
  "display": "standalone",
  "gcm_sender_id": "224273183921"
}</pre>

<p>同时,你也需要在HTML文档用一个{{HTMLElement("link")}} 标签里指向你的manifest文件:</p>

<pre class="brush: html">&lt;link rel="manifest" href="manifest.json"&gt;</pre>

<h3 id="userVisibleOnly参数">userVisibleOnly参数</h3>

<p>Chrome 要求你在订阅推送服务的时候设置 <a href="/en-US/docs/Web/API/PushManager/subscribe#Parameters"><code>userVisibleOnly 参数</code></a> 为 <code>true</code> , 表示我们承诺每当收到推送通知时都会显示通知。这可以在我们的<a href="https://github.com/chrisdavidmills/push-api-demo/blob/gh-pages/main.js#L127"> <code>subscribe()</code> 函数</a> 中看到。</p>

<h2 id="See_also">See also</h2>

<ul>
 <li><a href="/en-US/docs/Web/API/Push_API">Push API</a></li>
 <li><a href="/en-US/docs/Web/API/Service_Worker_API">Service Worker API</a></li>
</ul>

<div class="note">
<p><strong>Note</strong>: Some of the client-side code in our Push demo is heavily influenced by Matt Gaunt's excellent examples in <a href="http://updates.html5rocks.com/2015/03/push-notificatons-on-the-open-web">Push Notifications on the Open Web</a>. Thanks for the awesome work, Matt!</p>
</div>