日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747



前言

PWA (Progressive web Apps),漸進式Web 應用,又稱輕應用,是一種純html5網站卻可實現Native App的屏幕入口、離線緩存、消息推送等功能的W3C標準的技術組合。

PWA的完整教程網上比較少(中文版寫的比較好的:https://lavas.baidu.com/pwa,不過里面實踐比較少,很多坑沒踩出來),故寫下這篇文章幫助需要的人。PWA按照以上三個主要功能,分別用到三種技術:

manifest.json 實現APP入口

Service Worker 離線緩存

Web Push 消息推送

它們都需要在https基礎上才能使用。

PWA并不是新技術,早在2014年即有人提出草案并做出了demo,比微信小程序還早。隨著標準被新版本瀏覽器支持,17年國內也有很多團隊開始實踐,而18年前端Chrome力推的兩大前端技術就是PWA與Flutter。不同的是,PWA是力求不改變原站代碼的基礎上,逐步的實現輕應用的功能;而Flutter是用Dart重寫跨平臺的APP,一套代碼,多端使用。

理想很美好,現實很骨感。PWA在國內實踐并不算多,由兩個重要原因:1. 國內瀏覽器對之支持不太好。2. web push功能在國內遇阻,因為web push由瀏覽器自己的消息推送服務器實現的,比如Chrome的消息推送國內常常block。所以,為了更好的體驗,中國局域網用戶推薦使用Firefox, 其他互聯網用戶推薦使用Chrome(測試后發現,國內局域網也是部分能收到Chrome的推送)。

manifest.json 實現APP入口

manifest.json是一個位于網站對外根目錄的配置文件(一般與index.html在同級目錄),開發者只需按照 W3C定義好的屬性https://www.w3.org/TR/appmanifest/設置即可,本文不做詳述,只列舉幾個常用的屬性:

JavaScript技術棧應用篇之輕應用PWA實踐全過程,快速集成到應用

 

手機用戶可以用瀏覽器的“添加至主屏幕”,上述配置在此處生效,并且手機默認也會提示用戶去添加。

開發者可以在Chrome devTools 的Application的Manifest中查看當前網站的匹配,它還可以提示配置錯誤。

Service Worker 離線緩存

Service Worker 是運行于瀏覽器后臺的獨立線程,它注冊在指定源的路徑下,不僅不同網站都有獨立的Worker,同一個網站不同的路徑下也可以注冊不同的Worker,一旦注冊則是永久的,除非手動卸載,在Chrome devTools 的Application的Service Worker中可以查看/卸載。

可以發現Service Worker與Web Worker非常類似,都是獨立于主線程之外的獨立線程,都不能使用Window之類的瀏覽器內置對象,都不能操作DOM,都是異步的等。不僅如此,Service Worker還被增強了,它可以攔截/代理瀏覽器的請求,可以使用Cache Storage緩存頁面,可以監聽服務器推送的消息并且向在瀏覽器給用戶推送消息等。

使用Service Worker之前,我們先了解一下它的生命周期:

JavaScript技術棧應用篇之輕應用PWA實踐全過程,快速集成到應用

 


JavaScript技術棧應用篇之輕應用PWA實踐全過程,快速集成到應用

 

以上代碼寫在一個名為service_worker.js的腳本里,但它是獨立運行的,我們又需要寫引用/執行這個腳本的腳本 service_worker_before.js。

入口文件service_worker_before.js 注冊Service worker :

JavaScript技術棧應用篇之輕應用PWA實踐全過程,快速集成到應用

 

注冊代碼很簡單,需注意幾點:

a. scope是Worker的源的范圍,默認值為service_worker.js所在目錄。

b. 這里命名了swVersion 即Service Worker version,用它記錄與升級我們的Worker, 并把這個值傳入Worker中,控制著緩存的版本,我們讓緩存與Worker一起升級。但有一個問題,我們的頁面是會被緩存的,這時無論我們的版本號是多少,都無法讓其升級,所以對于升級代碼文件,我們不應該使用離線緩存,而應該使用瀏覽器默認的緩存,也可以直接設置不緩存。

c. 升級文件指 manifest.json, service_worker.js,service_worker_before.js。比如在Nginx中可以設置不要緩存(未實踐):

JavaScript技術棧應用篇之輕應用PWA實踐全過程,快速集成到應用

 

外部入口注冊后,我們可以在service_worker.js中寫Worker內部事件了:

Worker 安裝

JavaScript技術棧應用篇之輕應用PWA實踐全過程,快速集成到應用

 

如果追求快速更新,我們可以跳過等待,直接激活,即我們打開的新頁面都是使用最新的Worker代碼。

Worker 激活

JavaScript技術棧應用篇之輕應用PWA實踐全過程,快速集成到應用

 

激活之后,我們做了3件事:

a. 更新所有的同源客戶端的service_worker.js,即使它沒有刷新頁面。

b.清除非當前最新版本的cache。

c. 把首頁與離線頁面(根據自己的需要)進入立即緩存,如果不這么做的話,因為激活階段(第1次打開頁面)還沒到達,Worker還沒有開始做cache的工作,頁面已經打開了,這時是沒有離線緩存的,第2次打開頁面時沒有離線cache,但這時頁面會緩存下來,只有第3次才開始能取到離線cache,而上述這么做,第2次進來即可以拿到離線cache的首頁。offline.html則是離線狀態下的提示頁,否則用戶不知道可以離線緩存,就直接不再使用APP了。

Cache Storage 離線緩存

JavaScript技術棧應用篇之輕應用PWA實踐全過程,快速集成到應用

 

注意點:

a. Cache Storage與我們常說的瀏覽器緩存(Http Cache)有相似之處,即對整個請求/文件緩存。又有不同之處,它可永久保存,可離線使用。在在Chrome devTools -> Application -> Cache -> Cache Storage中可以查看。

b. fech事件可以攔截HTTPS的請求,進行緩存,但下次請求時如果發現已經緩存過,則直接返回緩存中的HTTPS Response,不過上述代碼沒有這么做,因為博客頁面非常小,為了追求頁面最新,只有當離線時才使用緩存,這種做法其實是偏離了離線緩存減小服務器壓力的的初衷。不過離線緩存與時時更新是矛盾的,取決于業務怎么權衡了。

c. 請求都是clone之后才緩存,因為請求的狀態是變化的,如果直接保存,可能不是當時的結果。

d. 只有Get請求才緩存,否則會報錯,畢竟像Post/Put/Delete之類的離線緩存也沒有意義。這里開發者可以自己定義規則。

e. 離線提示頁是在這里攔截而實現的。

f. 為了保證順利升級,我在緩存中設置的升文件“manifest.json”、“service_worker.js”,“service_worker_before.js”是不做離線緩存的。

Web Push 消息推送

Web Push的過程比較復雜,因為它涉及到4個端:

JavaScript技術棧應用篇之輕應用PWA實踐全過程,快速集成到應用

 

首先先列出簡化的9個步驟:

a. 業務服務端生成公鑰與私鑰,并把公鑰給網頁客戶端

b. 網頁客戶端需要支持PushManager前提下,然后請求用戶授權通知

c. b的基礎上,網頁客戶端把公鑰轉成Uint8Array

d. 網頁客戶端向推送服務端發起訂閱,如果成功,會得到推送服務器返回的訂閱信息

e. 網頁客戶端把訂閱信息發給業務服務端

f. 業務服務端保留該訂閱信息

g. 業務服務端拿著訂閱列表、公鑰私鑰、把想要推送的信息發送給推送服務端

h. 推送服務端拿到推送信息,解析后發送給Service Worker端

i. Service Worker監聽到信息,使用Notification推送給用戶

除了四個端之間有各種交互,還有各種加密比較麻煩外,關于推送服務器文檔少、不便于調試、兼容性不好也是個問題。

關于Web Push的php后端實現

本博客后端使用的PHP,相關教程較少,所幸已經開源的組件可用https://github.com/web-push-libs/web-push-php。

安裝minishlink/web-push

yum install php-gmp
composer require minishlink/web-push

可是安裝報錯:

The following exception is caused by a lack of memory or swap, or not having swap configured

Check https:// getcomposer。org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details

PHP Warning: proc_open(): fork failed - Cannot allocate memory in phar:// /usr/local/bin/composer/vendor/symfony/console/Application.php on line 952

Warning: proc_open(): fork failed - Cannot allocate memory in phar:// /usr/local/bin/composer/vendor/symfony/console/Application.php on line 952

[ErrorException]

proc_open(): fork failed - Cannot allocate memory

內存問題,修改后OK

/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=256
/sbin/mkswap /var/swap.1
/sbin/swapon /var/swap.1

a.生成公鑰私鑰

use MinishlinkWebPushVAPID;
echo var_dump(VAPID::createVapidKeys());

f. 業務服務端保留該訂閱信息

g. 業務服務端拿著訂閱列表、公鑰私鑰、把想要推送的信息發送給推送服務端

public function push_mess(Request $request)
{
 $title = $request->input('title');
 $body = $request->input('body');
 $href = $request->input('href');
 $noticeObj = new stdClass();
 $noticeObj->title = $title;
 $noticeObj->body = $body;
 $noticeObj->href = $href;
 $noticeObj->icon = "/static/dist/image/common/favicon.ico";
 $noticeObj->badge = "/static/dist/image/common/favicon.ico";
 $auth = array(
 'VAPID' => array(
 'subject' => 'https://www.boatsky.com/',
 'publicKey' => 'BGMKbiifiHo5zKaK+gQ=',
 'privateKey' => 'FjGJbNeg=',
 ),
 );
 $webPush = new WebPush($auth);
 $subList = DB::table(SUBSCRIPTION_TABLE_NAME)
 ->get();
 foreach($subList as $sub){
 $subscription = Subscription::create(array(
 'endpoint'=> $sub->endpoint,
 'publicKey'=> $sub->public_key,
 'authToken'=> $sub->auth_token,
 'contentEncoding'=> $sub->content_encoding
 ), true);
 $res = $webPush->sendNotification(
 $subscription,
 json_encode($noticeObj)
 );
 }
 // handle eventual errors here, and remove the subscription from your server if it is expired
 $pushResult = '';
 foreach ($webPush->flush() as $report) {
 $endpoint = $report->getRequest()->getUri()->__toString();
 if ($report->isSuccess()) {
 $pushResult = $pushResult . "[successfully] -- {$endpoint}.<br>";
 } else {
 $pushResult = $pushResult . "[failed]- {$endpoint}: {$report->getReason()}<br>";
 $deleteFlag = DB::table(SUBSCRIPTION_TABLE_NAME)->where('endpoint', $endpoint)->delete();
 echo var_dump($deleteFlag);
 if ($deleteFlag) {
 $pushResult = $pushResult . " delete success !<br>";
 }
 }
 }
 $resp = array(
 'errcode' => 0,
 'errmsg' => '',
 'data' => $pushResult
 );
 return response()->json($resp);
}

提交推送的信息頁面:

<section class="mod-inner">
 <form class="bsf-form" id="pushForm">
 <h2>推送消息</h2>
 <div class="bsf-unit">
 <label class="bsf-label" for="title">標題:</label>
 <input type="text" name="title" class="bsf-item" value="輕應用PWA實踐過程"/>
 </div>
 <div class="bsf-unit">
 <label class="bsf-label" for="body">內容:</label>
 <input type="text" name="body" class="bsf-item" value="技術·JS"/>
 </div>
 <div class="bsf-unit">
 <label class="bsf-label" for="href">鏈接:</label>
 <input type="text" name="href" class="bsf-item" value="https://www.boatsky.com/blog/66.html?cf=push"/>
 </div>
 <div class="bsf-unit">
 <label class="bsf-label"> </label>
 <button type="button" class="bsf-btn bsf-btn-primary bsf-btn-md" onclick="pushSubmit()">提交</button>
 </div>
 </form>
 <div id="pushResultMsg"></div>
</section>
function pushSubmit() {
 $.ajax({
 url : '/admin/push/push_mess',
 method : 'POST',
 headers: {
 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
 },
 data : $('#pushForm').serialize(),
 dataType : 'JSON',
 error : function(e){
 alert('error');
 },
 success : function(resp){
 if(resp.errcode === 0){
 $('#pushResultMsg').html(resp.data);
 }
 else {
 alert(resp.errmsg);
 }
 }
 });
}
</script>

只需使用上述HTML,即可以推送相關信息,并且加上其他配置,還可以設置有效時間,推送時間等。

Web Push 授權、發起訂閱、提交訂閱

if ('PushManager' in window) {
 if (Notification.permission !== 'granted') {
 // 請求授權
 askPermission();
 }
 // 發起訂閱
 navigator.serviceWorker.ready.then(function(reg) {subscribe(reg)});
}
// 授權消息推送
function askPermission() {
 return new Promise(function (resolve, reject) {
 var permissionResult = Notification.requestPermission(function (result) {
 resolve(result); // 舊版本
 });
 if (permissionResult) {
 permissionResult.then(resolve, reject); // 新版本
 }
 }).then(function (permissionResult) {
 if (permissionResult !== 'granted') {
 alert('只有允許顯示通知,您才能收到更新提醒,提醒一個月只會出現兩三次,您可以在設置處修改。');
 }
 }).catch(e => console.log(e));
}
// 將base64的applicationServerKey轉換成UInt8Array
function urlBase64ToUint8Array(base64String) {
 var padding = '='.repeat((4 - base64String.length % 4) % 4);
 var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
 var rawData = window.atob(base64);
 var outputArray = new Uint8Array(rawData.length);
 for (var i = 0, max = rawData.length; i < max; ++i) {
 outputArray[i] = rawData.charCodeAt(i);
 }
 return outputArray;
}
function subscribe(serviceWorkerReg) {
 serviceWorkerReg.pushManager.subscribe({ // 2. 訂閱
 userVisibleOnly: true,
 applicationServerKey: urlBase64ToUint8Array('BGMKbiifiMDHo5ZiXxziLuOC7GZaPGdDBfwZp4eYGUxUKvY1VMjNff814+Oi4jAQXnY1LMNgYahiV8gAzKaK+gQ=')
 }).then(function (subscription) {
 // 3. 發送推送訂閱對象到服務器,具體實現中發送請求到后端api
 sendEndpointInSubscription(subscription);
 console.log('subscribe success');
 }).catch(function (e) {
 console.log(e);
 // 訂閱請求失敗
 if (Notification.permission === 'denied') {
 }
 });
}
function sendEndpointInSubscription(subscription) {
 let endpoint = subscription.endpoint;
 let publicKey = subscription.getKey('p256dh');
 publicKey = publicKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(publicKey))) : null;
 let authToken = subscription.getKey('auth');
 authToken = authToken ? btoa(String.fromCharCode.apply(null, new Uint8Array(authToken))) : null;
 const contentEncoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0];
 const reqData = {
 endpoint,
 publicKey,
 authToken,
 contentEncoding,
 }
 console.log(reqData);
 $.ajax({
 url : '/admin/push/save_subscription',
 method : 'POST',
 headers: {
 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
 },
 data : reqData,
 dataType : 'JSON',
 error : function(e){
 },
 success : function(resp){
 console.log('send success');
 }
 });
}

endpoint: 為客戶端推薦的地址,推送服務端便是用這個找到客戶端的。

publicKey: 公鑰

authToken: 加密方式,好處是推送服務器也無法解密這個信息

contentEncoding: 編碼方式

Service Worker 監聽push,發出通知

// 監聽server有push的消息,通知用戶
self.addEventListener('push', function (event) {
 console.log('push', event);
 if (!(self.Notification && self.Notification.permission === 'granted')) {
 return;
 }
 if (event.data) {
 var promiseChain = Promise.resolve(event.data.json()).then(data => {
 console.log(data);
 // 使用setTimeout之后,可以實現點擊跳轉,否則chrome不行
 setTimeout(function(){
 self.registration.showNotification(data.title, {
 body: data.body,
 icon: data.icon,
 badge: data.badge,
 data: {
 href: data.href,
 }
 });
 }, 10);
 });
 event.waitUntil(promiseChain);
 }
});

self.registration.showNotification 中data是可以傳額外的參數。

有個細節,官方沒有提到的,需要用setTimeout包著showNotification,Chrome推送出的消息才不會出現鏈接無法點擊的問題。

監聽推送消息的點擊事件

// 推送消息點擊事件
self.addEventListener('notificationclick', event => {
 console.log('notificationclick');
 const clickedNotification = event.notification;
 const urlToOpen = new URL(clickedNotification.data.href, self.location.origin).href;
 let promiseChain = clients.matchAll({
 type: 'window',
 includeUncontrolled: true
 }).then(windowClients => {
 let matchingClient = null;
 for (let i = 0, max = windowClients.length; i < max; i++) {
 let windowClient = windowClients[i];
 if (windowClient.url.split('?')[0] === urlToOpen.split('?')[0]) {
 matchingClient = windowClient;
 break;
 }
 }
 return matchingClient ? matchingClient.focus() : clients.openWindow(urlToOpen);
 });
 event.waitUntil(promiseChain);
 clickedNotification.close();
});

監聽 notificationclick 點擊事件,除了需要打開彈窗,還要判斷該彈窗是否曾經打開過,如果是則只需active tab即可。

參考鏈接

https://www.boatsky.com/blog/66

分享到:
標簽:JavaScript PWA
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定