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
Flipkart Lite: FlipKart 是印度最大的电商之一。以下图编程
AliExpress:AliExpress 是个很是受欢迎的全球在线零售市场,经过实践 PWA以后,访问量和浏览数都成倍增长这里不作详细讲解。以下图json
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