Web開發多年來有了顯著的發展。它容許開發人員部署網站或Web應用程序並在數分鐘內爲全球數百萬人服務。只需一個瀏覽器,用戶能夠輸入URL就能夠訪問Web應用程序了。隨着 Progressive Web Apps的到來,開發人員可使用現代Web技術向用戶提供很好體驗的應用程序。在這篇文章中,你會學習到如何構建一個離線的漸進式 web 應用程序(Progressive Web Apps),下面就叫 PWA 啦。css
雖然不少文章已經說過了,已經理解的童鞋請跳過這個步驟。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
這類新的Web應用程序具備定義它們存在的特性。沒有很難的知識點,下面這些都是 PWA具備的一些特性:npm
Responsive(響應式):Ui能夠適配多個終端,桌面,手機,平板等等編程
App-like(像app):當與一個PWA交互時,它應該感受像一個原生的應用程序。json
Connectivity Independent(鏈接獨立): 它能離線瀏覽(經過 Service Workers) 或者在低網速上也能瀏覽
Re-engageable(從新鏈接):經過推送通知等功能,用戶應該可以持續地參與和重用應用程序。
Installable(安裝):用戶能夠添加在主屏幕而且從那裏啓動它他們就能夠從新應用程序了。
Discoverable(可發現的):用戶經過搜索應被識別發現的
Fresh(最新數據):當用戶鏈接到網絡時,應該可以在應用程序中提供新的內容。
Safe(安全):該經過HTTPS提供服務,防止內容篡改和中間人攻擊。
Progressive(漸進式):無論瀏覽器的選擇如何,它應該對每一個用戶都有效。
Linkable(可連接):經過URL分享給別人。
Flipkart Lite: FlipKart 是印度最大的電商之一。以下圖
AliExpress:AliExpress 是個很是受歡迎的全球在線零售市場,經過實踐 PWA以後,訪問量和瀏覽數都成倍增長這裏不作詳細講解。以下圖
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的生命週期。
// 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
指給緩存存儲的名稱
// 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)文件更改時都更新其緩存。
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 Specification 和 primer 都是關於Service Workers的一下很是有用的學習資料。
在文章的前面,我曾屢次提到過應用殼app shell
。應用程序殼是用最小的HTML,CSS和JavaScript驅動應用程序的用戶界面。一個PWA確保應用殼被緩存,以對應app
屢次快速訪問和快速加載。
咱們將構建一個簡單的PWA。這個app只跟蹤來自特定開源項目的最新提交。做爲一個 PWA,他應該具具備:
建立index.html
和latest.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.js
,toast.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.js
和 app.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 頁面
爲了確保咱們的app快速加載和離線工做,咱們須要緩存app shell
經過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-toolbox
和 sw-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提供離線頁面。另外一種選擇是在第一次加載時緩存提交數據,在後續請求中加載本地保存的數據,而後在用戶鏈接時獲取最新的數據。提交的數據能夠存儲在IndexedDB
或local 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