H5 PWA技術以及小demo

H5 PWA技術

一、原生app優缺點css

  a、體驗好、下載到手機上入口方便html

  b、開發成本高(ios和安卓)前端

  c、軟件上線須要審覈android

  d、版本更新須要將新版本上傳到不一樣的應用商店ios

  e、使用前需下載git

 


 

 

二、web網頁優缺點github

  a、開發成本低、網站更新時上傳最新的資源到服務器便可、手機自帶瀏覽器打開便可web

  b、體驗比原生app差chrome

  c、入口不便捷express

  d、無網無相應,不具有離線能力

  e、無app的消息推送

 


 

 

三、PWA是什麼?

PWA是一個新的前端技術,全稱:Progressive Web App,這是一個漸進式的網頁應用程序。結合了一系列現代web技術,在網頁應用中實現和原生應用相近的用戶體驗。

 

PWA的三個關鍵詞:

  Reliable(可靠的):當用戶從手機屏幕啓動時,無需考慮網絡狀態,能夠馬上加載出PWA

  Fast(快速的):加載速度快

  Engaging(可參與的):PWA能夠添加在用戶的主屏幕上,無需從應用商店裏下載,他們經過網絡應用程序Manifest file提供相似於APP的使用體驗(android上可設置全屏顯示,因爲Safari支持度的問題ios不能夠),能夠進行「推送通知」

 

小小總結:

  a、解決的問題:

      1>可添加至主屏幕

      2>實現離線緩存功能

      3>實現消息推送

  b、優點:幾乎瞬間加載,但安全且富有彈性

  c、核心:manifest文件清單、Service Workers

 


 

 

四、Manifest

做用:  

  a、可以將你瀏覽的網頁添加到你的手機屏幕上

​  b、在 Android 上可以全屏啓動,不顯示地址欄 ( 因爲 Iphone 手機的瀏覽器是 Safari ,因此不支持)

  c、控制屏幕 橫屏 / 豎屏 展現

​  d、定義啓動畫面

​  e、能夠設置你的應用啓動是從主屏幕啓動仍是從 URL 啓動

​  f、能夠設置你添加屏幕上的應用程序圖標、名字、圖標大小

 

示例:

index.html

<head>
  <title>Minimal PWA</title>
  <meta name="viewport" content="width=device-width, user-scalable=no" />
  <link rel="manifest" href="manifest.json" />
  <link rel="stylesheet" type="text/css" href="main.css">
  <link rel="icon" href="/e.png" type="image/png" />
</head>

manifest.json

{
  "name": "Minimal PWA", // 必填 顯示的插件名稱
  "short_name": "PWA Demo", // 可選  在APP launcher和新的tab頁顯示,若是沒有設置,則使用name
  "description": "The app that helps you understand PWA", //用於描述應用
  "display": "standalone", // 定義開發人員對Web應用程序的首選顯示模式。standalone模式會有單獨的
  "start_url": "/", // 應用啓動時的url
  "theme_color": "#313131", // 桌面圖標的背景色
  "background_color": "#313131", // 爲web應用程序預約義的背景顏色。在啓動web應用程序和加載應用程序的內容之間建立了一個平滑的過渡。
  "icons": [ // 桌面圖標,是一個數組
    {
    "src": "icon/lowres.webp",
    "sizes": "48x48",  // 以空格分隔的圖片尺寸
    "type": "image/webp"  // 幫助userAgent快速排除不支持的類型
  },
  {
    "src": "icon/lowres",
    "sizes": "48x48"
  },
  {
    "src": "icon/hd_hi.ico",
    "sizes": "72x72 96x96 128x128 256x256"
  },
  {
    "src": "icon/hd_hi.svg",
    "sizes": "72x72"
  }
  ]
}

另附:

Manifest參考文檔:https://developer.mozilla.org/zh-CN/docs/Web/Manifest

能夠打開網站https://developers.google.cn/web/showcase/2015/chrome-dev-summit查看添加至主屏幕的動圖。

 

 


 

 

五、Service Worker

SW 是什麼呢?這個是離線緩存文件。咱們 PWA 技術使用的就是它!SW 是瀏覽器在後臺獨立於網頁運行的腳本,它打開了通向不須要網頁或用戶交互的功能的大門,由於使用了它,纔會有的那個 Reliable 特性吧,SW 做用於 瀏覽器於服務器之間,至關於一個代理服務器。

 

  • 基本特色
  • 運行在它本身的全局腳本上下文中
  • 不綁定到具體的網頁
  • 沒法修改網頁中的元素,由於它沒法訪問 DOM
  • 只能使用 HTTPS
  • 攔截進出的 HTTP 請求,從而徹底控制你的網站
  • 與主JS線程獨立,不會被阻塞
  • 徹底異步,沒法使用localStorage

 

功能(仍是比較逆天的)

  • 後臺數據的同步
  • 從其餘域獲取資源請求
  • 接受計算密集型數據的更新,多頁面共享該數據
  • 客戶端編譯與依賴管理
  • 後端服務的hook機制
  • 根據URL模式,自定義模板
  • 性能優化
  • 消息推送
  • 定時默認更新
  • 地理圍欄

 

生命週期:

  • Parsed ( 解析成功 ): 首次註冊 SW 時,瀏覽器解決腳本並得到入口點,若是解析成功,就能夠訪問到 SW 註冊對象,在這一點中咱們須要在 HTML 頁面中添加一個判斷,判斷該瀏覽器是否支持 SW 。
  • Installing ( 正在安裝 ):SW 腳本解析完成以後,瀏覽器會嘗試進行安裝,installing 中 install 事件被執行,若是其中有 event.waitUntil ( ) 方法,則 installing 事件會一直等到該方法中的 Promise 完成以後纔會成功,若是 Promise 被拒絕,則安裝失敗,SW會進入 Redundant( 廢棄 )狀態。
  • Installed / Waiting (安裝成功/等待中):若是安裝成功,SW 將會進入這個狀態。
  • Activating ( 正在激活 ):處於 waiting 狀態的 SW 發生如下狀況,將會進入 activating 狀態中:

    當前已無激活狀態的 worker 、 SW腳本中的 self.skipWaiting()方法被調用 ( ps: self 是 SW 中做用於全局的對象,這個方法根據英文翻譯過來也能明白什麼意思啦,跳過等待狀態 )、用戶已關閉 SW 做用域下的全部頁面,從而釋放了當前處於激活狀態的 worker、超出指定時間,從而釋放當前處於激活狀態的 worker

  • Activated ( 激活成功 ):該狀態,其成功接收了 document 全面控制的激活態 worker 。
  • Redundant ( 廢棄 ):這個狀態的出現時有緣由的,若是 installing 事件失敗或者 activating 事件失敗或者新的 SW 替換其成爲激活態 worker 。installing 事件失敗和 activating 事件失敗的信息咱們能夠在 Chrome 瀏覽器的 DevTools 中查看

 

若是上個圖很差理解,能夠看這個,把它的生命週期當作紅綠燈:

  • register (須要下載和解析,紅燈)
  • install (執行,黃燈)
  • activated( 成功,綠燈)

  

當用戶首次導航至 URL 時,服務器會返回響應的網頁

  • 第1步:當你調用 register() 函數時, Service Worker 開始下載。
  • 第2步:在註冊過程當中,瀏覽器會下載、解析並執行 Service Worker ()。若是在此步驟中出現任何錯誤,register() 返回的 promise 都會執行 reject 操做,而且 Service Worker 會被廢棄。
  • 第3步:一旦 Service Worker 成功執行了,install 事件就會激活
  • 第4步:安裝完成,Service Worker 便會激活,並控制在其範圍內的一切。若是生命週期中的全部事件都成功了,Service Worker 便已準備就緒,隨時可使用了!

 


 

 

六、實現離線緩存

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello Caching World!</title>
  </head>
  <body>
    <!-- Image -->
    <img src="/images/hello.png" />                 
    <!-- JavaScript -->
    <script async src="/js/script.js"></script>     
    <script>
      // 註冊 service worker
      if ('serviceWorker' in navigator) {           
        navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function (registration) {
          // 註冊成功
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function (err) {                   
          // 註冊失敗 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

注意:

注:Service Worker 的註冊路徑決定了其 scope 默認做用頁面的範圍。
若是 service-worker.js 是在 /sw/ 頁面路徑下,這使得該 Service Worker 默認只會收到 頁面/sw/ 路徑下的 fetch 事件。
若是存放在網站的根路徑下,則將會收到該網站的全部 fetch 事件。
若是但願改變它的做用域,可在第二個參數設置 scope 範圍。示例中將其改成了根目錄,即對整個站點生效。

 

service-worker.js

var cacheName = 'helloWorld';     // 緩存的名稱  
// install 事件,它發生在瀏覽器安裝並註冊 Service Worker 時        
self.addEventListener('install', event => { 
/* event.waitUtil 用於在安裝成功以前執行一些預裝邏輯
 可是建議只作一些輕量級和很是重要資源的緩存,減小安裝失敗的機率
 安裝成功後 ServiceWorker 狀態會從 installing 變爲 installed */
  event.waitUntil(
    caches.open(cacheName)                  
    .then(cache => cache.addAll([    // 若是全部的文件都成功緩存了,便會安裝完成。若是任何文件下載失敗了,那麼安裝過程也會隨之失敗。        
      '/js/script.js',
      '/images/hello.png'
    ]))
  );
});
  
/**
爲 fetch 事件添加一個事件監聽器。接下來,使用 caches.match() 函數來檢查傳入的請求 URL 是否匹配當前緩存中存在的任何內容。若是存在的話,返回緩存的資源。
若是資源並不存在於緩存當中,經過網絡來獲取資源,並將獲取到的資源添加到緩存中。
*/
self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request)                  
    .then(function (response) {
      if (response) {                            
        return response;                         
      }
      var requestToCache = event.request.clone();  //          
      return fetch(requestToCache).then(                   
        function (response) {
          if (!response || response.status !== 200) {      
            return response;
          }
          var responseToCache = response.clone();          
          caches.open(cacheName)                           
            .then(function (cache) {
              cache.put(requestToCache, responseToCache);  
            });
          return response;             
    })
  );
});

注意:

注:爲何用request.clone()和response.clone()
須要這麼作是由於request和response是一個流,它只能消耗一次。由於咱們已經經過緩存消耗了一次,而後發起 HTTP 請求還要再消耗一次,因此咱們須要在此時克隆請求
Clone the request—a request is a stream and can only be consumed once.

 


 

 

七、消息推送

  • 步驟1、提示用戶並得到他們的訂閱詳細信息
  • 步驟2、將這些詳細信息保存在服務器上
  • 步驟3、在須要時發送任何消息

前兩步:

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Progressive Times</title>
    <link rel="manifest" href="/manifest.json">                                      
  </head>
  <body>
    <script>
      var endpoint;
      var key;
      var authSecret;
      var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
      // 方法很複雜,可是能夠不用具體看,知識用來轉化vapidPublicKey用
      function urlBase64ToUint8Array(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;
      }
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js').then(function (registration) {
          return registration.pushManager.getSubscription()                            
            .then(function (subscription) {
              if (subscription) {                                                      
                return;
              }
              return registration.pushManager.subscribe({                              
                  userVisibleOnly: true,
                  applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
                })
                .then(function (subscription) {
                  var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                  key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
                  var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                  authSecret = rawAuthSecret ?
                    btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
                  endpoint = subscription.endpoint;
                  return fetch('./register', {                                         
                    method: 'post',
                    headers: new Headers({
                      'content-type': 'application/json'
                    }),
                    body: JSON.stringify({
                      endpoint: subscription.endpoint,
                      key: key,
                      authSecret: authSecret,
                    }),
                  });
                });
            });
        }).catch(function (err) {
          // 註冊失敗 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

步驟三:

app.js

const webpush = require('web-push');                 
const express = require('express');
var bodyParser = require('body-parser');
const app = express();
webpush.setVapidDetails(                             
  'mailto:contact@deanhume.com',
  'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
  'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);
app.post('/register', function (req, res) {           
  var endpoint = req.body.endpoint;
  saveRegistrationDetails(endpoint, key, authSecret); 
  const pushSubscription = {                          
    endpoint: req.body.endpoint,
    keys: {
      auth: req.body.authSecret,
      p256dh: req.body.key
    }
  };
  var body = 'Thank you for registering';
  var iconUrl = 'https://example.com/images/homescreen.png';
  // 發送 Web 推送消息
  webpush.sendNotification(pushSubscription,          
      JSON.stringify({
        msg: body,
        url: 'http://localhost:3111/',
        icon: iconUrl
      }))
    .then(result => res.sendStatus(201))
    .catch(err => {
      console.log(err);
    });
});
app.listen(3111, function () {
  console.log('Web push app listening on port 3111!')
});

service worker監聽push事件,將通知詳情推送給用戶

service-worker.js

self.addEventListener('push', function (event) {
 // 檢查服務端是否發來了任何有效載荷數據
  var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
  var title = 'Progressive Times';
  event.waitUntil(
    // 使用提供的信息來顯示 Web 推送通知
    self.registration.showNotification(title, {                           
      body: payload.msg,
      url: payload.url,
      icon: payload.icon
    })
  );
});

 


 

 

八、PWA小demo

準備:

  建立一個關於PWA項目的文件夾

  文件夾內準備一張圖

  一個index.html文件

  一個main.css文件

  一個manifest.json文件

  一個sw.js文件

 

之後更新把,寫崩了,我再尋思尋思

 


 

 

分割線,可愛的我來更新小栗子了 

 

css文件夾裏有一個style.css

images裏有一個logo.jpg

 

具體看代碼:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello PWA</title>
    <link rel="stylesheet" href="css/style.css">

</head>
<body>
    <img src="images/logo.jpg">

    <script>
        if ('serviceWorker' in navigator) { // 瀏覽器支持SW
            navigator.serviceWorker.register('sw.js').then(function (registration) {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch(function (err) {
                console.log('ServiceWorker registration failed: ', err);
            });
        }
    </script>
</body>
</html>

個人style.css是空的哈哈哈

sw.js

var cacheName = 'hello-pwa';

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(cacheName)
          .then(cache => cache.addAll(
            [
                '/',  // 這個必定要包含整個目錄,否則沒法離線瀏覽
                './images/logo.jpg',
                './index.html',
                './css/style.css'
            ]
          )).then(() => self.skipWaiting())
    );
});

self.addEventListener('fetch', function (event) {  
    event.respondWith(
      caches.match(event.request)                    
      .then(function (response) {
        if (response) {                              
          return response;                           
        }
        return fetch(event.request);                 
      })
    );
});

 

接下來經過 http-server 和 ngrok(https)進行調試查看

在當前文件下 
安裝 http-server

npm install http-server -g

安裝 ngrok,下載解壓便可

在項目目錄下執行以下命令:

http-server -c-1  // -c-1 會關閉緩存

再開啓另一個終端在 ngrok 文件的目錄下執行以下命令:

./ngrok http 8080 // http-server 默認開啓8080端口

 

運行:(我端口8080被佔了,因此這裏是8081)

 

 查看application

查看緩存部分

第一次加載進來緩存沒有東西,須要刷新一下頁面。

https://github.com/yangTwo100/PWA_search_demo

以上。

相關文章
相關標籤/搜索