1. 程式人生 > >【PWA】web推送技術

【PWA】web推送技術

伴隨著今年 Google I/O 大會的召開,一個很火的概念–Progressive Web Apps 誕生了。這代表著我們 web 端有了和原生 APP 媲美的能力。但是,有一個很重要的痛點,web 一直不能使用訊息推送,雖然,後面提出了 Notification API,但這需要網頁持續開啟,這對於常規 APP 實現的推送,根本就不是一個量級的。所以,開發者一直在呼籲能不能退出一款能夠在網頁關閉情況下的 web 推送呢? 現在,Web 時代已經到來! 為了做到在網頁關閉的情況下,還能繼續傳送 Notification,我們就只能使用駐留程序。而現在 Web 的駐留程序就是現在正在大力普及的 

Service Worker。換句話說,我們的想要實現斷線 Notification 的話,需要用的技術棧是:

  • Push
  • Notification
  • Service Worker

這裡,我先一個簡單的 demo 樣式。

noti

說實在的,我其實 TM 很煩的這 Noti。一般使用 PC 端的,也沒見有啥訊息彈出來,但是,現在好了 Web 一搞,結果三端通用。你如果不禁用的話,保不準天天彈。。。

SW(Service Worker) 我已經在前一篇文章裡面講清楚了。這裡主要探究一下另外兩個技術 Push 和 Notification。首先,有一個問題,這兩個技術是用來幹嘛的呢?

Push && Notification

這兩個技術,我們可以理解為就是 server 和 SW 之間,SW 和 user 之間的訊息通訊。

  • push: server 將更新的資訊傳遞給 SW
  • notification: SW 將更新的資訊推送給使用者

可以看出,兩個技術是緊密連線到一起的。這裡,我們先來講解一下 notification 的相關技術。

Notification

那現在,我們想給使用者傳送一個訊息的話應該怎麼傳送呢? 程式碼很簡單,我直接放了:

self.addEventListener('push', function(event) {
  var title = 'Yay a message.';
  var body = 'We have received a push message.'
; var icon = '/images/icon-192x192.png'; var tag = 'simple-push-demo-notification-tag'; var data = { doge: { wow: 'such amaze notification data' } }; event.waitUntil( self.registration.showNotification(title, { body: body, icon: icon, tag: tag, data: data }) ); });

大家一開始看見這個程式碼,可能會覺得有點陌生。實際上,這裡是結合 SW 來完成的。push是 SW 接收到後臺的 push 資訊然後出發。當然,我們獲取資訊的主要途徑也是從 event 中獲取的。這裡為了簡便,就直接使用寫死的資訊了。大致解釋一下 API。

  • event.waitUntil(promise): 該方法是用來延遲 SW 的結束。因為,SW 可能在任何時間結束,為了防止這樣的情況,需要使用 waitUntil 監聽 promise,使系統不會在 promise 執行時就結束 SW。
  • ServiceWorkerRegistration.showNotification(title, [options]): 該方法執行後,會發回一個 promise 物件。

不過,我們需要記住的是 SW 中的 notification 只是很早以前就退出的桌面 notification 的繼承物件。這意味著,大家如果想要嘗試一下 notification,並不需要手動建立一個 notification,而只要使用

// 桌面端
var not = new Notification("show note", { icon: "newsong.svg", tag: "song" });
not.onclick = function() { dosth(this); };

// 在 SW 中使用
self.registration.showNotification("New mail from Alice", {
  actions: [{action: 'archive', title: "Archive"}]
});

self.addEventListener('notificationclick', function(event) {
  event.notification.close();
  if (event.action === 'archive') {
    silentlyArchiveEmail();
  } else {
    clients.openWindow("/inbox");
  }
}, false);

不過,如果你想設定自己想要的 note 效果的話,則需要了解一下,showNotification 裡面具體每次引數代表的含義,參考 Mozilla,我們可以瞭解到基本的使用方式。如上,API 的基本格式為 showNotification(title, [options])

  • title: 很簡單,就是該次 Not(Notification) 的標題
  • options: 這個而是一個物件,裡面可以接受很多引數。
    • actions[Array]:該物件是一個數組,裡面包含一個一個物件元素。每個物件包含內容為:
      • action[String]: 表示該 Not 的行為。後面是通過監聽 notificationclick 來進行相關處理
      • title[String]: 該 action 的標題
      • icon[URL]: 該 action 顯示的 logo。大小通常為 24*24

actions 的上限值,通常根據 Notification.maxActions 確定。通過在 Not 中定義好 actions 觸發,最後我們會通過,監聽的 notificationclick 來做相關處理:

self.addEventListener('notificationclick', function(event) {  
  var messageId = event.notification.data;
  
  event.notification.close();
    // 通過設定的 actions 來做適當的響應
  if (event.action === 'like') {  
    silentlyLikeItem();  
  }  
  else if (event.action === 'reply') {  
    clients.openWindow("/messages?reply=" + messageId);  
  }  
  else {  
    clients.openWindow("/messages?reply=" + messageId);  
  }  
}, false);
    • body[String]: Not 顯示的主體資訊
    • dir[String]: Not 顯示資訊的方向,通常可以取:auto, ltr, or rtl
    • icon[String]:Not 顯示的 Icon 圖片路徑。
    • image[String]:Not 在 body 裡面附帶顯示的圖片 URL,大小最好是 4:3 的比例。
    • tag[String]:用來標識每個 Not。方便後續對 Not 進行相關管理。
    • renotify[Boolean]:當重複的 Not 觸發時,標識是否禁用振動和聲音,預設為 false
    • vibrate[Array]:用來設定振動的範圍。格式為:[振動,暫停,振動,暫停…]。具體取值單位為 ms。比如:[100,200,100]。振動 100ms,靜止 200ms,振動 100ms。這樣的話,我們可以設定自己 APP 都有的振動提示頻率。
    • sound[String]: 設定音訊的地址。例如: /audio/notification-sound.mp3
    • data[Any]: 用來附帶在 Not 裡面的資訊。我們一般可以在 notificationclick 事件中,對回撥引數進行呼叫event.notification.data

針對於推送的圖片來說,可能會針對不同的手機用到的圖片尺寸會有所區別,例如,針對不同的 dpi。

具體參照:

cut

看下 MDN 提供的 demo:

function showNotification() {
  Notification.requestPermission(function(result) {
    if (result === 'granted') {
      navigator.serviceWorker.ready.then(function(registration) {
        registration.showNotification('Vibration Sample', {
          body: 'Buzz! Buzz!',
          icon: '../images/touch/chrome-touch-icon-192x192.png',
          vibrate: [200, 100, 200, 100, 200, 100, 200],
          tag: 'vibration-sample'
        });
      });
    }
  });
}

當然,簡單 API 的使用就是上面那樣。但是,如果我們不加剋制的使用 Not,可能會讓使用者完全遮蔽掉我們的推送,得不償失。所以,我們需要遵循一定的原則去傳送。

推送原則

  1. 推送必須簡潔 遵循時間,地點,人物要素進行相關資訊的設定。

  2. 儘量不要讓使用者開啟網頁檢視 雖然這看起來有點違揹我們最初的意圖。不過,這樣確實能夠提高使用者的體驗。比如在資訊回覆中,直接顯示:XX回覆:... 這樣的格式,可以完全省去使用者的開啟網頁的麻煩。

  3. 不要在 title 和 body 出現一樣的資訊 比如: correct: first incorrect 此處輸入圖片的描述

  4. 不要推薦原生 APP 因為很有可能造成推送資訊重複

  5. 不要寫上自己的網址 因為,Not 已經幫你寫好了 website_name

  6. 儘量讓 icon 和推送有關聯 沒用的 icon: icon 實用的 icon: icon

推送許可權

實際上,Not 並不全在 SW 中執行,對於設計使用者初始許可權,我們需要在主頁面中,做出相關的響應。當然,在設定推送的時候,我們需要考慮到使用者是否會禁用,這裡影響還是特別大的。 我們,獲取使用者許可權一般可以直接使用 Notification 上掛載的 permission 屬性來獲取的。

  • defualt: 表示需要進行詢問。預設情況是不顯示推送
  • denied: 不顯示推送
  • granted: 顯示推送

簡單的來說為:

function initialiseState() {
  if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
    return;
  }

  // 檢查是否可以進行伺服器推
  if (!('PushManager' in window)) {
    return;
  }

  // 是否被禁用
  if (Notification.permission === 'denied') {
    return;
  }

  if (Notification.permission === 'granted') {
    // dosth();
    return;
  }

  // 如果還處於預設情況下,則進行詢問
  navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
    // 檢查訂閱
    serviceWorkerRegistration.pushManager.getSubscription()
      .then(function(subscription) {
        // 檢查是否已經被訂閱
        if (!subscription) {
          // 沒有
          return;
        }
        // 有
        // doSth();
      })
      .catch(function(err) {
        window.Demo.debug.log('Error during getSubscription()', err);
      });
  });
}

我們在載入的時候,需要先進行檢查一遍,如果是預設情況,則需要發起訂閱的請求。然後再開始進行處理。 那,我們上面的那段程式碼該放在哪個位置呢?首先,這裡使用到了 SW,這意味著,我們需要將 SW 先註冊成功才行。實際程式碼應放在 SW 註冊成功的回撥中:

window.addEventListener('load', function() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./service-worker.js')
    .then(initialiseState);
  } else {
    window.Demo.debug.log('Service workers aren\'t supported in this browser.');
  }
});

為了更好的顯示資訊,我們還可以將授權程式碼放到後面去。比如,將 subscribe 和 btn 的 click 事件進行繫結。這時候,我們並不需要考慮 SW 是否已經註冊好了,因為SW 的註冊時間遠遠不及使用者的反應時間。 例如:

  var pushButton = document.querySelector('.js-push-button');
  pushButton.addEventListener('click', function() {
    if (isPushEnabled) {
      unsubscribe();
    } else {
      subscribe();
    }
  });

我們具體看一下 subscribe 內容:

function subscribe() {
  var pushButton = document.querySelector('.js-push-button');
  pushButton.disabled = true;

  navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
    // 請求訂閱
    serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true})
      .then(function(subscription) {
        isPushEnabled = true;
        pushButton.textContent = 'Disable Push Messages';
        pushButton.disabled = false;
        return sendSubscriptionToServer(subscription);
      })
  });
}

說道這裡,大家可能會看的雲裡霧裡,這裡我們來具體看一下 serviceWorkerRegistration.pushManager 具體含義。該引數是從 SW 註冊事件回撥函式獲取的。也就是說,它是我們和 SW 互動的通道。該物件上,綁定了幾個獲取訂閱相關的 API:

  • subscribe(options) [Promise]: 該方法就是我們常常用來觸發詢問的 API。他返回一個 promise 物件.回撥引數為 pushSubscription 物件。這裡,我們後面再進行討論。這裡主要說一下 options 裡面有哪些內容
    • options[Object]
      • userVisibleOnly[Boolean]:用來表示後續資訊是否展示給使用者。通常設定為 true.
      • applicationServerKey: 一個 public key。用來加密 server 端 push 的資訊。該 key 是一個 Uint8Array 物件。

例如:

registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: new Uint8Array([...])
    });
  • getSubscription() [Promise]: 用來獲取已經訂閱的 push subscription 物件。
  • permissionState(options) [Promise]: 該 API 用來獲取當前網頁訊息推送的狀態 ‘prompt’, ‘denied’, 或 ‘granted’。裡面的 options 和 subscribe 裡面的內容一致。

為了更好的體驗,我們可以將兩者結合起來,進行相關推送檢查,具體的 load 中,則為:

window.addEventListener('load', function() {
  var pushButton = document.querySelector('.js-push-button');
  pushButton.addEventListener('click', function() {
    if (isPushEnabled) {
      unsubscribe();
    } else {
      subscribe();
    }
  });
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./service-worker.js')
    .then(initialiseState);
  } else {
    window.Demo.debug.log('Service workers aren\'t supported in this browser.');
  }
});

當然,這裡面還會涉及其他的一些細節,我這裡就不過多贅述了。詳情可以查閱: Notification demo

我們開啟一個 Not 詢問很簡單,但關鍵是,如果讓使用者同意。如果我們一開始就進行詢問,這樣成功性的可能性太低。我們可以在頁面載入後進行詢問。這裡,也有一些提醒原則:

  1. 通過具體行為進行詢問 比如,當我在查詢車票時,就可以讓使用者在退出時選擇是否接受推送資訊。比如,國外的飛機延遲通知網頁: delay

  2. 讓使用者來決定是否進行推送 因為使用者不是技術人員,我們需要將一些介面,暴露給使用者。針對推送而言,我們可以讓使用者選擇是否進行推送,並且,在提示的同時,顯示的資訊應該儘量和使用者相關。 user

推送處理

web push 在實際協議中,會設計到兩個 server,比較複雜,這裡我們先來看一下。client 是如何處理接受到的資訊的。 當 SW 接受到 server 傳遞過來的資訊時,會先觸發 push 事件。我們通常做如下處理:

self.addEventListener('push', function(event) { 
if (event.data) {
    console.log('This push event has data: ',event.data.text()); 
    } else {
    console.log('This push event has no data.');
  }
});

其中,我們通過 server push 過來的 msg 通常是掛載到 event.data 裡的。並且,該部署了 Response 的相關 API:

  • text(): 返回 string 的內容
  • json(): 返回 經過 json parse 的物件
  • blob(): 返回 blob 物件
  • arrayBuffer(): 返回 arrayBuffer 物件

我們知道 Service Worker 並不是常駐程序,有童鞋可能會問到,那怎麼利用 SW 監聽 push 事件呢? 這裡就不用擔心了,因為瀏覽器自己會開啟一個埠監聽接受到的資訊,然後喚起指定的 SW(如果你的瀏覽器是關閉的,那麼你可以洗洗睡了)。而且,由於這樣隨機關閉的機制,我們需要上述提到的 event.waitUntil API 來幫助我們完成持續 alive SW 的效果,防止正在執行的非同步程式被終止。針對於我們的 notification 來說,實際上就是一個非同步,所以,我們需要使用上述 API 進行包裹。

self.addEventListener('push', function(event) {
const promiseChain = self.registration.showNotification('Hello, World.');
  event.waitUntil(promiseChain);
});

當然,如果你想在 SW 裡面做更多的非同步事情的話,可以使用 Promise.all 進行包裹。

self.addEventListener('push', function(event) {
    const promiseChain = Promise.all([ async1,async2 ]);
  event.waitUntil(promiseChain);
});

之後,就是將具體資訊展示推送給使用者了。上面已經將了具體 showNotification 裡面的引數有哪些。不過,這可能不夠直觀,我們可以使用一張圖來感受一下:

cur

(左:firefox,右:Chrome)

另外,在 showNotification options 裡面,還有一些屬性需要我們額外注意。

屬性注意

tag

對於指定的 Not 我們可以使用 tag 來表明其唯一性,這代表著當我們在使用相同 tag 的 Not 時,上一條 Not 會被最新擁有同一個 tag 的Not 替換。即:

    const title = 'First Notification';
    const options = {
      body: 'With \'tag\' of \'message-group-1\'',
      tag: 'message-group-1'
    };
    registration.showNotification(title, options);

顯示樣式為:

first

接著,我顯示一個不同 tag 的 Not: