[譯]介紹一下漸進式 Web App(離線) - Part 1

Web開發多年來有了顯著的發展。它容許開發人員部署網站或Web應用程序並在數分鐘內爲全球數百萬人服務。只需一個瀏覽器,用戶能夠輸入URL就能夠訪問Web應用程序了。隨着 Progressive Web Apps的到來,開發人員可使用現代Web技術向用戶提供很好體驗的應用程序。在這篇文章中,你會學習到如何構建一個離線的漸進式 web 應用程序(Progressive Web Apps),下面就叫 PWA 啦。css

首先介紹一下什麼是 PWA

雖然不少文章已經說過了,已經理解的童鞋請跳過這個步驟。PWA基本上是使用現代Web技術構建的網站,可是體驗上卻像一個移動 app,在2015年,谷歌工程師Alex Russell和Frances Berriman創造了「 Progressive Web Apps」。此後谷歌就一直致力於讓 PWA能給用戶像原生 app通常的體驗。一個典型的PWA的應該是這樣的:html

一、開始在Web瀏覽器的地址欄中訪問git

二、有顯示添加到設備的主屏幕選項github

三、逐步開始展現諸如離線使用、推送通知和後臺同步等應用程序屬性。web

到目前爲止,移動APP能夠作不少Web App不能真正作的事情。PWA,一個web app嘗試去作移動app已經很長時間了。它結合最好的 web技術的和最好的app技術,能夠在慢速網絡鏈接上快速加載,離線瀏覽,推送消息,並在Web屏幕上加載Web應用程序入口。shell

到如今,安卓上最心版本的Chrome瀏覽器,支持在桌面上快速打開你的 web app 了,這一切都感謝 PWA,以下圖express

WPA 的特性

這類新的Web應用程序具備定義它們存在的特性。沒有很難的知識點,下面這些都是 PWA具備的一些特性:npm

  • Responsive(響應式):Ui能夠適配多個終端,桌面,手機,平板等等編程

  • App-like(像app):當與一個PWA交互時,它應該感受像一個原生的應用程序。json

  • Connectivity Independent(鏈接獨立): 它能離線瀏覽(經過 Service Workers) 或者在低網速上也能瀏覽

  • Re-engageable(從新鏈接):經過推送通知等功能,用戶應該可以持續地參與和重用應用程序。

  • Installable(安裝):用戶能夠添加在主屏幕而且從那裏啓動它他們就能夠從新應用程序了。

  • Discoverable(可發現的):用戶經過搜索應被識別發現的

  • Fresh(最新數據):當用戶鏈接到網絡時,應該可以在應用程序中提供新的內容。

  • Safe(安全):該經過HTTPS提供服務,防止內容篡改和中間人攻擊。

  • Progressive(漸進式):無論瀏覽器的選擇如何,它應該對每一個用戶都有效。

  • Linkable(可連接):經過URL分享給別人。

PWA的一些生產用例

Flipkart Lite: FlipKart 是印度最大的電商之一。以下圖

AliExpress:AliExpress 是個很是受歡迎的全球在線零售市場,經過實踐 PWA以後,訪問量和瀏覽數都成倍增長這裏不作詳細講解。以下圖

Service Workers

Service Workers是可編程代理的一個script腳本運行在你瀏覽器的後臺,它具備攔截、處理HTTP請求的能力,也能以各類方式對他們做出響應。它有響應網絡請求、推送通知、鏈接更改等等的功能。Service Workers不能訪問DOM,但它能夠利用獲取和緩存API。您能夠Service Workers緩存全部靜態資源,這將自動減小網絡請求並提升性能。 Service worker 能夠顯示一個 app應用殼,通知用戶,他們與互聯網斷開了而且提供一個頁面供用戶在離線時進行交互、瀏覽。

一個Service worker文件,例如sw.js須要像這樣放置在根目錄中:

在你的PWA中開始service workers,若是你的應用程序的JS文件是app.js,你須要去註冊service workers在你的app.js文件,下面的代碼就是註冊你的service workers。

if ('serviceWorker' in navigator) {
    navigator.serviceWorker
             .register('./sw.js')
             .then(function() { console.log('Service Worker Registered'); });
  }
複製代碼

上面的代碼檢查瀏覽器是否支持service workers。若是支持,就開始註冊service workers,一旦service workers註冊了,咱們就開始體驗用戶第一次訪問頁面時service workers的生命週期。

service workers的生命週期

  • Install:在用戶第一次訪問頁面時觸發安裝事件。在這個階段中,service workers被安裝在瀏覽器中。在安裝過程當中,您能夠將Web app的全部靜態資產緩存下來。以下面代碼所示:
// Install Service Worker
self.addEventListener('install', function(event) {

    console.log('Service Worker: Installing....');

    event.waitUntil(

        // Open the Cache
        caches.open(cacheName).then(function(cache) {
            console.log('Service Worker: Caching App Shell at the moment......');

            // Add Files to the Cache
            return cache.addAll(filesToCache);
        })
    );
});
複製代碼

filesToCache變量表明的全部文件要緩存數組

cachename指給緩存存儲的名稱

  • Activate:當service worker啓動時,此事件將被觸發。
// Fired when the Service Worker starts up
self.addEventListener('activate', function(event) {

    console.log('Service Worker: Activating....');

    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(cacheNames.map(function(key) {
                if( key !== cacheName) {
                    console.log('Service Worker: Removing Old Cache', key);
                    return caches.delete(key);
                }
            }));
        })
    );
    return self.clients.claim();
});
複製代碼

在這裏, service worker每當應用殼(app shell)文件更改時都更新其緩存。

  • Fetch:此事件做用與於從服務器端的數據緩存到 app殼中。caches.match()解析了觸發Web請求的事件,並檢查它是否在緩存中得到數據。而後,它要麼響應於緩存版本的數據,要麼用fetch從網絡中獲取數據。用 e.respondWith()方法來響應返回到Web頁面。
self.addEventListener('fetch', function(event) {

    console.log('Service Worker: Fetch', event.request.url);

    console.log("Url", event.request.url);

    event.respondWith(
        caches.match(event.request).then(function(response) {
            return response || fetch(event.request);
        })
    );
});
複製代碼

在寫代碼的時候。咱們須要注意一下Chrome, Opera、Firefox是支持service workers 的,可是Safari 和 Edge 尚未兼容到service workers

Service Worker Specificationprimer 都是關於Service Workers的一下很是有用的學習資料。

Application Shell(應用殼)

在文章的前面,我曾屢次提到過應用殼app shell。應用程序殼是用最小的HTML,CSS和JavaScript驅動應用程序的用戶界面。一個PWA確保應用殼被緩存,以對應app屢次快速訪問和快速加載。

下面咱們將逐步寫一個 PWA例子

咱們將構建一個簡單的PWA。這個app只跟蹤來自特定開源項目的最新提交。做爲一個 PWA,他應該具具備:

  • 離線應用,用戶應該可以在沒有Internet鏈接的狀況下查看最新提交。
  • 應用程序應當即加載重複訪問
  • 打開按鈕通知按鈕後,用戶將得到對最新提交到開放源代碼項目的通知。
  • 可安裝(添加到主屏幕)
  • 有一個Web應用程序清單

光說不作瞎扯淡,開始吧!

建立index.htmllatest.html文件在你的代碼文件夾裏面。

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Commits PWA</title>
  <link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
    <div class="app app__layout">
      <header>
        <span class="header__icon">
          <svg class="menu__icon no--select" width="24px" height="24px" viewBox="0 0 48 48" fill="#fff">
            <path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z"></path>
          </svg>
        </span>

        <span class="header__title no--select">PWA - Home</span>
      </header>

      <div class="menu">
        <div class="menu__header"></div>
        <ul class="menu__list">
          <li><a href="index.html">Home</a></li>
          <li><a href="latest.html">Latest</a></li>
      </div>

      <div class="menu__overlay"></div>

      <div class="app__content">

        <section class="section">
          <h3> Stay Up to Date with R-I-L </h3>
          <img class="profile-pic" src="./images/books.png" alt="Hello, World!">

          <p class="home-note">Latest Commits on Resources I like!</a></p>
        </section>


        <div class="fab fab__push">
          <div class="fab__ripple"></div>
          <img class="fab__image" src="./images/push-off.png" alt="Push Notification" />
        </div>

        <!-- Toast msg's --> <div class="toast__container"></div> </div> </div> <script src="./js/app.js"></script> <script src="./js/toast.js"></script> <script src="./js/offline.js"></script> <script src="./js/menu.js"></script> </body> </html> 複製代碼

latest.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Commits PWA</title>
  <link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
    <div class="app app__layout">
      <header>
        <span class="header__icon">
          <svg class="menu__icon no--select" width="24px" height="24px" viewBox="0 0 48 48" fill="#fff">
            <path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z"></path>
          </svg>
        </span>
        <span class="header__title no--select">PWA - Commits</span>
      </header>

      <div class="menu">
        <div class="menu__header"></div>
        <ul class="menu__list">
          <li><a href="index.html">Home</a></li>
          <li><a href="latest.html">Latest</a></li>
        </ul>
      </div>

      <div class="menu__overlay"></div>

      <section class="card_container">
        <h2 style="margin-top:70px;" align="center">Latest Commits!</h2>



        <div class="container">
            <section class="card first">

            </section>
            <section class="card second">

            </section>
            <section class="card third">

            </section>
            <section class="card fourth">

            </section>
            <section class="card fifth">

            </section>
        </div>
      </section>

       <div class="loader">
          <svg viewBox="0 0 32 32" width="32" height="32">
            <circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle>
          </svg>
        </div>

      <!-- Toast msg's --> <div class="toast__container"></div> </div> <script src="./js/app.js"></script> <script src="./js/latest.js"></script> <script src="./js/toast.js"></script> <script src="./js/offline.js"></script> <script src="./js/menu.js"></script> </body> </html> 複製代碼

建立一個 css 文件夾,而且在這個文件下載建立一個style.css文件(能夠點擊這裏查看),建立一個js文件夾,並在這個文件下建立app.js, menu.js, offline.js, latest.jstoast.js

js/offline.js

(function () {
  'use strict';

  var header = document.querySelector('header');
  var menuHeader = document.querySelector('.menu__header');

  //After DOM Loaded
  document.addEventListener('DOMContentLoaded', function(event) {
    //On initial load to check connectivity
    if (!navigator.onLine) {
      updateNetworkStatus();
    }

    window.addEventListener('online', updateNetworkStatus, false);
    window.addEventListener('offline', updateNetworkStatus, false);
  });

  //To update network status
  function updateNetworkStatus() {
    if (navigator.onLine) {
      header.classList.remove('app__offline');
      menuHeader.style.background = '#1E88E5'; 
    }
    else {
      toast('You are now offline..');
      header.classList.add('app__offline');
      menuHeader.style.background = '#9E9E9E';
    }
  }
})();
複製代碼

上面的代碼幫助用戶在 ui視覺上區分離線和在線狀態。

js/menu.js

(function () {
  'use strict';

  var menuIconElement = document.querySelector('.header__icon');
  var menuElement = document.querySelector('.menu');
  var menuOverlayElement = document.querySelector('.menu__overlay');

  //Menu click event
  menuIconElement.addEventListener('click', showMenu, false);
  menuOverlayElement.addEventListener('click', hideMenu, false);
  menuElement.addEventListener('transitionend', onTransitionEnd, false);

   //To show menu
  function showMenu() {
    menuElement.style.transform = "translateX(0)";
    menuElement.classList.add('menu--show');
    menuOverlayElement.classList.add('menu__overlay--show');
  }

  //To hide menu
  function hideMenu() {
    menuElement.style.transform = "translateX(-110%)";
    menuElement.classList.remove('menu--show');
    menuOverlayElement.classList.remove('menu__overlay--show');
    menuElement.addEventListener('transitionend', onTransitionEnd, false);
  }

  var touchStartPoint, touchMovePoint;

  /*Swipe from edge to open menu*/

  //`TouchStart` event to find where user start the touch
  document.body.addEventListener('touchstart', function(event) {
    touchStartPoint = event.changedTouches[0].pageX;
    touchMovePoint = touchStartPoint;
  }, false);

  //`TouchMove` event to determine user touch movement
  document.body.addEventListener('touchmove', function(event) {
    touchMovePoint = event.touches[0].pageX;
    if (touchStartPoint < 10 && touchMovePoint > 30) {          
      menuElement.style.transform = "translateX(0)";
    }
  }, false);

  function onTransitionEnd() {
    if (touchStartPoint < 10) {
      menuElement.style.transform = "translateX(0)";
      menuOverlayElement.classList.add('menu__overlay--show');
      menuElement.removeEventListener('transitionend', onTransitionEnd, false); 
    }
  }
})();

複製代碼

上面的代碼做用於菜單省略號按鈕的動畫。

js/toast.js

(function (exports) {
  'use strict';

  var toastContainer = document.querySelector('.toast__container');
 
  //To show notification
  function toast(msg, options) {
    if (!msg) return;

    options = options || 3000;

    var toastMsg = document.createElement('div');
    
    toastMsg.className = 'toast__msg';
    toastMsg.textContent = msg;

    toastContainer.appendChild(toastMsg);

    //Show toast for 3secs and hide it
    setTimeout(function () {
      toastMsg.classList.add('toast__msg--hide');
    }, options);

    //Remove the element after hiding
    toastMsg.addEventListener('transitionend', function (event) {
      event.target.parentNode.removeChild(event.target);
    });
  }

  exports.toast = toast; //Make this method available in global
})(typeof window === 'undefined' ? module.exports : window);
複製代碼

上面的代碼是是一個 tost 的提示信息框

latest.jsapp.js 如今仍是空的。

如今,使用本地服務器啓動你的應用程序,例如 http-server模塊能夠幫組你啓動本地服務,您的Web應用程序應該以下所示:

Side menu

Index Page

Latest Page

Application Shell

您的應用殼也在上面突出顯示。如今還沒有實現加載動態內容,下一步,咱們須要從 Github's API獲取最新的提交。

獲取動態內容

打開js/latest.js增長下面的代碼

(function() {
  'use strict';

  var app = {
    spinner: document.querySelector('.loader')
  };

  var container = document.querySelector('.container');


  // Get Commit Data from Github API
  function fetchCommits() {
    var url = 'https://api.github.com/repos/unicodeveloper/resources-i-like/commits';

    fetch(url)
    .then(function(fetchResponse){ 
      return fetchResponse.json();
    })
    .then(function(response) {

        var commitData = {
            'first': {
              message: response[0].commit.message,
              author: response[0].commit.author.name,
              time: response[0].commit.author.date,
              link: response[0].html_url
            },
            'second': {
              message: response[1].commit.message,
              author: response[1].commit.author.name,
              time: response[1].commit.author.date,
              link: response[1].html_url
            },
            'third': {
              message: response[2].commit.message,
              author: response[2].commit.author.name,
              time: response[2].commit.author.date,
              link: response[2].html_url
            },
            'fourth': {
              message: response[3].commit.message,
              author: response[3].commit.author.name,
              time: response[3].commit.author.date,
              link: response[3].html_url
            },
            'fifth': {
              message: response[4].commit.message,
              author: response[4].commit.author.name,
              time: response[4].commit.author.date,
              link: response[4].html_url
            }
        };

        container.querySelector('.first').innerHTML = 
        "<h4> Message: " + response[0].commit.message + "</h4>" +
        "<h4> Author: " + response[0].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[0].commit.author.date)).toUTCString() +  "</h4>" +
        "<h4>" + "<a href='" + response[0].html_url + "'>Click me to see more!</a>"  + "</h4>";

        container.querySelector('.second').innerHTML = 
        "<h4> Message: " + response[1].commit.message + "</h4>" +
        "<h4> Author: " + response[1].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[1].commit.author.date)).toUTCString()  +  "</h4>" +
        "<h4>" + "<a href='" + response[1].html_url + "'>Click me to see more!</a>"  + "</h4>";

        container.querySelector('.third').innerHTML = 
        "<h4> Message: " + response[2].commit.message + "</h4>" +
        "<h4> Author: " + response[2].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[2].commit.author.date)).toUTCString()  +  "</h4>" +
        "<h4>" + "<a href='" + response[2].html_url + "'>Click me to see more!</a>"  + "</h4>";

        container.querySelector('.fourth').innerHTML = 
        "<h4> Message: " + response[3].commit.message + "</h4>" +
        "<h4> Author: " + response[3].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[3].commit.author.date)).toUTCString()  +  "</h4>" +
        "<h4>" + "<a href='" + response[3].html_url + "'>Click me to see more!</a>"  + "</h4>";

        container.querySelector('.fifth').innerHTML = 
        "<h4> Message: " + response[4].commit.message + "</h4>" +
        "<h4> Author: " + response[4].commit.author.name + "</h4>" +
        "<h4> Time committed: " + (new Date(response[4].commit.author.date)).toUTCString() +  "</h4>" +
        "<h4>" + "<a href='" + response[4].html_url + "'>Click me to see more!</a>"  + "</h4>";

        app.spinner.setAttribute('hidden', true); //hide spinner
      })
      .catch(function (error) {
        console.error(error);
      });
  };

  fetchCommits();
})();

複製代碼

此外在你的latest.html引入latest.js

<script src="./js/latest.js"></script>
複製代碼

增長 loading 在你的latest.html

....
<div class="loader">
      <svg viewBox="0 0 32 32" width="32" height="32">
        <circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle>
      </svg>
</div>

<div class="toast__container"></div>
複製代碼

latest.js你能夠觀察到,咱們從GitHub的API獲取到數據並將其附加到DOM中來,如今獲取後的頁面長這樣子了。

Latest.html 頁面

經過Service Workers預加載 app shell

爲了確保咱們的app快速加載和離線工做,咱們須要緩存app shell經過service worker

  • 首先,在根目錄中建立一個 service worker文件。它的名字sw.js
  • 第二,打開app.js文件和添加這段代碼來實現service worker註冊使用

app.js

if ('serviceWorker' in navigator) {
     navigator.serviceWorker
             .register('./sw.js')
             .then(function() { console.log('Service Worker Registered'); });
  }
複製代碼
  • 打開sw.js文件並添加這段代碼

sw.js

var cacheName = 'pwa-commits-v3';

var filesToCache = [
    './',
    './css/style.css',
    './images/books.png',
    './images/Home.svg',
    './images/ic_refresh_white_24px.svg',
    './images/profile.png',
    './images/push-off.png',
    './images/push-on.png',
    './js/app.js',
    './js/menu.js',
    './js/offline.js',
    './js/toast.js'
];

// Install Service Worker
self.addEventListener('install', function(event) {

    console.log('Service Worker: Installing....');

    event.waitUntil(

        // Open the Cache
        caches.open(cacheName).then(function(cache) {
            console.log('Service Worker: Caching App Shell at the moment......');

            // Add Files to the Cache
            return cache.addAll(filesToCache);
        })
    );
});


// Fired when the Service Worker starts up
self.addEventListener('activate', function(event) {

    console.log('Service Worker: Activating....');

    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(cacheNames.map(function(key) {
                if( key !== cacheName) {
                    console.log('Service Worker: Removing Old Cache', key);
                    return caches.delete(key);
                }
            }));
        })
    );
    return self.clients.claim();
});


self.addEventListener('fetch', function(event) {

    console.log('Service Worker: Fetch', event.request.url);

    console.log("Url", event.request.url);

    event.respondWith(
        caches.match(event.request).then(function(response) {
            return response || fetch(event.request);
        })
    );
});
複製代碼

就像我在這篇文章的前面部分所解釋的,咱們全部的靜態資源都放到filesToCache數組裏面,當service worker被安裝時,它在瀏覽器中打開緩存而且數組裏面全部文件都被緩存到pwa-commits-v3這個緩存裏面。一旦 service worker已經安裝,install事件就會觸發。此階段確保您的service worker在任何應用殼文件更改時更新其緩存。fetch事件階段應用殼從緩存中獲取數據。 注意:爲了更容易和更好的方式預先高速緩存你的資源。檢查谷歌瀏覽器的sw-toolboxsw-precachelibraries

如今重載你的 web app 而且打開 DevTools,到Application選項去查看Service Worker面板,確保Update on reload這個選項是勾選的。以下圖

如今,從新加載Web頁面並檢查它。有離線瀏覽麼?

Index Page Offline

Yaaay!!! 首頁終於離線也是能夠瀏覽了,那麼latest頁面是否是顯示最新的提交呢?

Latest Page Offline

Yaaay!!!latest已經是離線服務。可是等一下!數據在哪裏?提交在哪裏?哎呀!咱們的 app試圖請求Github API當用戶與Internet斷開鏈接時,它失敗了。

Data Fetch Failure, Chrome DevTools

咱們該怎麼辦?處理這個場景有不一樣的方法。其中一個選項是告訴service worker提供離線頁面。另外一種選擇是在第一次加載時緩存提交數據,在後續請求中加載本地保存的數據,而後在用戶鏈接時獲取最新的數據。提交的數據能夠存儲在IndexedDBlocal Storage

好了,咱們如今就此結束!

附上:

原文地址: https://auth0.com/blog/introduction-to-progressive-apps-part-one/

項目代碼地址:https://github.com/unicodeveloper/pwa-commits

博客文章:https://blog.naice.me/articles/5a31d20a78c3ad318b837f59

若是有那個地方翻譯出錯或者失誤,請各位大神不吝賜教,小弟感激涕零

期待下一篇: 介紹一下漸進式 Web App(即時加載) - Part 2

相關文章
相關標籤/搜索