近来有许多关于 Progressive Web Apps(PWAs)的音讯,许多人都在问这是不是是(挪动)web 的将来。我不想堕入native app 和 PWA 的纷争,然则有一件事是肯定的 — PWA极大的提升了挪动端表现,改良了用户体验。
好音讯是开辟一个 PWA 并不难。事实上,我们能够将现存的网站举行革新,使之成为PWA。这也是我这篇文章要讲的 — 当你读完这篇文章,你能够将你的网站革新,让他看起来就像是一个 native web app。他能够离线事情而且具有本身的主屏图标。
Progressive Web Apps 是什么?
Progressive Web Apps (下文以“PWAs”代指) 是一个令人兴奋的前端手艺的革新。PWAs综合了一系列手艺使你的 web app表现得就像是 native mobile app。比拟于纯 web 解决方案和纯 native 解决方案,PWAs关于开辟者和用户有以下长处:
你只须要基于开放的 W3C 规范的 web 开辟手艺来开辟一个app。不须要多客户端开辟。
用户能够在装置前就体验你的 app。
不须要经由过程 AppStore 下载 app。app 会自动晋级不须要用户晋级。
用户会遭到‘装置’的提示,点击装置会增添一个图标到用户首屏。
被翻开时,PWA 会展现一个有吸引力的闪屏。
chrome 供应了可选选项,能够使 PWA 获得全屏体验。
必要的文件会被当地缓存,因而会比规范的web app 相应更快(也许也会比native app相应快)
装置及其轻量 — 也许会有几百 kb 的缓存数据。
网站的数据传输必需是 https 衔接。
PWAs 能够离线事情,而且在收集恢复时能够同步最新数据。
如今还处在 PWA 的初期,但已有 许多胜利案例 。
PWA 手艺现在被 Firefox,Chrome 和其他基于Blink内核的阅读器支撑。微软正在勤奋在Edge阅读器上完成。Apple没有行动 although there are promising comments in the WebKit five-year plan。荣幸的是,阅读器支撑关于 PWA 好像不太重要…
PWAs 是渐进加强的
你的app依旧能够运转在不支撑 PWA 手艺的阅读器里。用户不能离线接见,不过其他功用都像本来一样没有影响。综合利弊得失,没有来由不把你的 app 革新为 PWA。
不只是 Apps
Google 引领了 PWA 的一系列行动,所以大多半教程都在说怎样从零开始构建一个基于 Chrome,native-looking mobile app。然则并非只要特别的单页运用能够PWA化,也不须要肯定遵照 material interface design guidelines。大多半网站都能够在数小时内完成 PWA 化。这包括你的 WordPress站点或许静态站点。
示例代码
示例代码能够在https://github.com/sitepoint-editors/pwa-retrofit找到。
代码供应了一个简朴的四个页面的网站。个中包括一些图片,一个样式表和一个main javascript 文件。这个网站能够运转在一切当代阅读器上(IE10+)。假如阅读器支撑 PWA 手艺,当离线时用户能够阅读他们之前看过的页面。
运转代码前,确保 Node.js 已装置,然后再命令行里启动效劳:
node ./server.js [port]
[port]
是可设置的,默以为 8888。翻开 Chrome 或许其他基于Blink内核的阅读器,比方 Opera 或许 Vivaldi,然后输入链接 http://localhost:8888/(或许你指定的某个端口)。你也能够翻开开辟者东西看一下各个console信息。
阅读主页,或许其他页面,然后用以下任一要领使页面离线:
按下 Cmd/Ctrl + C ,住手 node 效劳器,或许
在开辟者东西的 Network 或许 Application – Service Workers 栏里点击 offline 选项。
从新阅读恣意之前阅读过的页面,它们依旧能够阅读到。阅读一个之前没有看过的页面,你会看到一个特地的离线页面,标识“you’re offline”,另有一个你能够阅读的页面列表:
衔接手机
你也能够经由过程 USB 衔接你的安卓手机来预览示例网页。在开辟者东西中翻开 Remote devices 菜单。
在左侧挑选 Settings ,点击 Add Rule 输入 8888 端口。你能够在你的手机上翻开Chrome,翻开 http://localhost:8888/。
你能够点击阅读器菜单里的 “Add to Home screen”。阅读几个页面,阅读器会提示你去装置。这两种体式格局都能够建立一个新的图标在你的主屏上。阅读几个页面后关掉Chrome,断开装备衔接。你依旧能够翻开 PWA Website app — 你会看到一个启动页,而且能够离线接见之前你接见过的页面。
将你的网站革新为一个 Progressive Web App 总共有三个必要步骤:
第一步:开启 HTTPS
由于一些不言而喻的缘由,PWAs 须要 HTTPS 衔接。
HTTPS 在示例代码中并非必需的,由于 Chrome 许可运用 localhost 或许任何 127.x.x.x 的地点来测试。你也能够在 HTTP 衔接下测试你的 PWA,你须要运用 Chrome ,而且输入以下命令行参数:
--user-data-dir
--unsafety-treat-insecure-origin-as-secure
第二步:建立一个 Web App Manifest
manifest 文件供应了一些我们网站的信息,比方 name,description 和须要在主屏运用的图标的图片,启动屏的图片等。
manifest文件是一个 JSON 花样的文件,位于你项目标根目录。它必需用Content-Type: application/manifest+json
或许 Content-Type: application/json
如许的 HTTP 头来要求。这个文件能够被命名为任何名字,在示例代码中他被命名为 /manifest.json
:
{
"name" : "PWA Website",
"short_name" : "PWA",
"description" : "An example PWA website",
"start_url" : "/",
"display" : "standalone",
"orientation" : "any",
"background_color" : "#ACE",
"theme_color" : "#ACE",
"icons": [
{
"src" : "/images/logo/logo072.png",
"sizes" : "72x72",
"type" : "image/png"
},
{
"src" : "/images/logo/logo152.png",
"sizes" : "152x152",
"type" : "image/png"
},
{
"src" : "/images/logo/logo192.png",
"sizes" : "192x192",
"type" : "image/png"
},
{
"src" : "/images/logo/logo256.png",
"sizes" : "256x256",
"type" : "image/png"
},
{
"src" : "/images/logo/logo512.png",
"sizes" : "512x512",
"type" : "image/png"
}
]
}
在页面的<head>
中引入:
<link rel="manifest" href="/manifest.json">
manifest 中重要属性有:
name —— 网页显现给用户的完全称号
short_name —— 当空间不足以显现全名时的网站缩写称号
description —— 关于网站的详细形貌
start_url —— 网页的初始 相对 URL(比方
/
)scope —— 导航局限。比方,
/app/
的scope就限定 app 在这个文件夹里。background-color —— 启动屏和阅读器的背景色彩
theme_color —— 网站的主题色彩,平常都与背景色彩雷同,它能够影响网站的显现
orientation —— 首选的显现方向:
any
,natural
,landscape
,landscape-primary
,landscape-secondary
,portrait
,portrait-primary
, 和portrait-secondary
。display —— 首选的显现体式格局:
fullscreen
,standalone
(看起来像是native app),minimal-ui
(有简化的阅读器掌握选项) 和browser
(通例的阅读器 tab)icons —— 定义了
src
URL,sizes
和type
的图片对象数组。
MDN供应了完全的manifest属性列表:Web App Manifest properties
在开辟者东西中的 Application tab 左侧有 Manifest 选项,你能够考证你的 manifest JSON 文件,并供应了 “Add to homescreen”。
第三步:建立一个 Service Worker
Service Worker 是阻拦和相应你的收集要求的编程接口。这是一个位于你根目录的一个零丁的 javascript 文件。
你的 js 文件(在示例代码中是 /js/main.js
)能够搜检是不是支撑 Service Worker,而且注册:
if ('serviceWorker' in navigator) {
// register service worker
navigator.serviceWorker.register('/service-worker.js');
}
假如你不须要离线功用,能够简朴的建立一个空的 /service-worker.js
文件 —— 用户会被提示装置你的 app。
Service Worker 很庞杂,你能够修正示例代码来到达本身的目标。这是一个规范的 web worker,阅读器用一个零丁的线程来下载和实行它。它没有挪用 DOM 和其他页面 api 的才能,但他能够阻拦收集要求,包括页面切换,静态资本下载,ajax要求所引发的收集要求。
这就是须要 HTTPS 的最重要的缘由。设想一下第三方代码能够阻拦来自其他网站的 service worker, 将是一个灾害。
service worker 重要有三个事宜: install,activate 和 fetch。
Install 事宜
这个事宜在app被装置时触发。它常常用来缓存必要的文件。缓存经由过程 Cache API来完成。
起首,我们来组织几个变量:
缓存称号(
CACHE
)和版本号(version
)。你的运用能够有多个缓存然则只能援用一个。我们设置了版本号,如许当我们有严重更新时,我们能够更新缓存,而疏忽旧的缓存。一个离线页面的URL(
offlineURL
)。当离线时用户试图接见之前未缓存的页面时,这个页面会显现给用户。一个具有离线功用的页面必要文件的数组(
installFilesEssential
)。这个数组应当包括静态资本,比方 CSS 和 JavaScript 文件,但我也把主页面(/
)和图标文件写进去了。假如主页面能够多个URL接见,你应当把他们都写进去,比方/
和/index.html
。注重,offlineURL
也要被写入这个数组。可选的,形貌文件数组(
installFilesDesirable
)。这些文件都很会被下载,但假如下载失利不会中断装置。
// configuration
const
version = '1.0.0',
CACHE = version + '::PWAsite',
offlineURL = '/offline/',
installFilesEssential = [
'/',
'/manifest.json',
'/css/styles.css',
'/js/main.js',
'/js/offlinepage.js',
'/images/logo/logo152.png'
].concat(offlineURL),
installFilesDesirable = [
'/favicon.ico',
'/images/logo/logo016.png',
'/images/hero/power-pv.jpg',
'/images/hero/power-lo.jpg',
'/images/hero/power-hi.jpg'
];
installStaticFiles()
要领增加文件到缓存,这个要领用到了基于 promise的 Cache API。当必要的文件都被缓存后才会天生返回值。
// install static assets
function installStaticFiles() {
return caches.open(CACHE)
.then(cache => {
// cache desirable files
cache.addAll(installFilesDesirable);
// cache essential files
return cache.addAll(installFilesEssential);
});
}
末了,我们增加install
的事宜监听函数。 waitUntil
要领确保一切代码实行终了后,service worker 才会实行 install。实行 installStaticFiles()
要领,然后实行 self.skipWaiting()
要领使service worker进入 active状况。
// application installation
self.addEventListener('install', event => {
console.log('service worker: install');
// cache core files
event.waitUntil(
installStaticFiles()
.then(() => self.skipWaiting())
);
});
Activate 事宜
当 install完成后, service worker 进入active状况,这个事宜马上实行。你能够不须要完成这个事宜监听,然则示例代码在这里删除老旧的无用缓存文件:
// clear old caches
function clearOldCaches() {
return caches.keys()
.then(keylist => {
return Promise.all(
keylist
.filter(key => key !== CACHE)
.map(key => caches.delete(key))
);
});
}
// application activated
self.addEventListener('activate', event => {
console.log('service worker: activate');
// delete old caches
event.waitUntil(
clearOldCaches()
.then(() => self.clients.claim())
);
});
注重,末了的self.clients.claim()
要领设置本身为active的service worker。
Fetch 事宜
当有收集要求时这个事宜被触发。它挪用respondWith()
要领来挟制 GET 要求并返回:
缓存中的一个静态资本。
假如 #1 失利了,就用 Fetch API(这与 service worker 的fetch 事宜没紧要)去收集要求这个资本。然后将这个资本到场缓存。
假如 #1 和 #2 都失利了,那就返回一个恰当的值。
// application fetch network data
self.addEventListener('fetch', event => {
// abandon non-GET requests
if (event.request.method !== 'GET') return;
let url = event.request.url;
event.respondWith(
caches.open(CACHE)
.then(cache => {
return cache.match(event.request)
.then(response => {
if (response) {
// return cached file
console.log('cache fetch: ' + url);
return response;
}
// make network request
return fetch(event.request)
.then(newreq => {
console.log('network fetch: ' + url);
if (newreq.ok) cache.put(event.request, newreq.clone());
return newreq;
})
// app is offline
.catch(() => offlineAsset(url));
});
})
);
});
末了这个offlineAsset(url)
要领经由过程几个辅佐函数返回一个恰当的值:
// is image URL?
let iExt = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].map(f => '.' + f);
function isImage(url) {
return iExt.reduce((ret, ext) => ret || url.endsWith(ext), false);
}
// return offline asset
function offlineAsset(url) {
if (isImage(url)) {
// return image
return new Response(
'<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>',
{ headers: {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'no-store'
}}
);
}
else {
// return page
return caches.match(offlineURL);
}
}
offlineAsset()
要领搜检是不是是一个图片要求,假如是,那末返回一个带有 “offline” 字样的 SVG。假如不是,返回 offlineURL
页面。
开辟者东西供应了检察 Service Worker 相干信息的选项:
在开辟者东西的 Cache Storage 选项列出了一切当前域内的缓存和所包括的静态文件。当缓存更新的时刻,你能够点击左下角的革新按钮来更新缓存:
不出预料, Clear storage 选项能够删除你的 service worker 和缓存:
再来一步 – 第四步:建立一个可用的离线页面
离线页面能够是一个静态页面,来申明当前用户要求不可用。然则,我们也能够在这个页面上列出能够接见的页面链接。
在main.js
中我们能够运用 Cache API 。然则API 运用promises,在不支撑的阅读器中会引发一切javascript运转壅塞。为了防止这类状况,我们在加载另一个 /js/offlinepage.js
文件之前必需搜检离线文件列表和是不是支撑 Cache API 。
// load script to populate offline page list
if (document.getElementById('cachedpagelist') && 'caches' in window) {
var scr = document.createElement('script');
scr.src = '/js/offlinepage.js';
scr.async = 1;
document.head.appendChild(scr);
}
/js/offlinepage.js
locates the most recent cache by version name, 取到一切 URL的key的列表,移除一切无用 URL,排序一切的列表而且把他们加到 ID 为cachedpagelist
的 DOM 节点中:
// cache name
const
CACHE = '::PWAsite',
offlineURL = '/offline/',
list = document.getElementById('cachedpagelist');
// fetch all caches
window.caches.keys()
.then(cacheList => {
// find caches by and order by most recent
cacheList = cacheList
.filter(cName => cName.includes(CACHE))
.sort((a, b) => a - b);
// open first cache
caches.open(cacheList[0])
.then(cache => {
// fetch cached pages
cache.keys()
.then(reqList => {
let frag = document.createDocumentFragment();
reqList
.map(req => req.url)
.filter(req => (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL))
.sort()
.forEach(req => {
let
li = document.createElement('li'),
a = li.appendChild(document.createElement('a'));
a.setAttribute('href', req);
a.textContent = a.pathname;
frag.appendChild(li);
});
if (list) list.appendChild(frag);
});
})
});
开辟东西
假如你以为 javascript 调试难题,那末 service worker 也不会很好。Chrome的开辟者东西的 Application 供应了一系列调试东西。
你应当翻开 隐身窗口 来测试你的 app,如许在你封闭这个窗口以后缓存文件就不会保留下来。
末了,Lighthouse extension for Chrome 供应了许多革新 PWA 的有效信息。
PWA 圈套
有几点须要注重:
URL 隐蔽
我们的示例代码隐蔽了 URL 栏,我不引荐这类做法,除非你有一个单 url 运用,比方一个游戏。关于多半网站,manifest 选项 display: minimal-ui
或许 display: browser
是最好的挑选。
缓存太多
你能够缓存你网站的一切页面和一切静态文件。这关于一个小网站是可行的,但这关于上千个页面的大型网站现实吗?没有人会对你网站的一切内容都感兴趣,而装备的内存容量将是一个限定。纵然你像示例代码一样只缓存接见过的页面和文件,缓存大小也会增进的很快。
也许你须要注重:
只缓存重要的页面,相似主页,和近来的文章。
不要缓存图片,视频和其他大型文件
常常删除旧的缓存文件
供应一个缓存按钮给用户,让用户决议是不是缓存
缓存革新
在示例代码中,用户在要求收集前先搜检该文件是不是缓存。假如缓存,就运用缓存文件。这在离线状况下很棒,但也意味着在联网状况下,用户获得的能够不是最新数据。
静态文件,相似于图片和视频等,不会常常转变的资本,做长时候缓存没有很大的题目。你能够在HTTP 头里设置 Cache-Control
来缓存文件使其缓存时候为一年(31,536,000 seconds):
Cache-Control: max-age=31536000
页面,CSS和 script 文件会常常变化,所以你应当改设置一个很短的缓存时候比方 24 小时,并在联网时与效劳端文件举行考证:
Cache-Control: must-revalidate, max-age=86400