张鑫旭-鑫空间-鑫生活
it's my whole life!备份内容浏览
将网站改进为增强型网页应用-PWA
译者 | 京东金融-移动研发部-前端工程师 郑莉
最近有一些关于增强型网页应用(PWA)的议论,很多人都质疑PWA是否代表了未来的(移动)Web。我并无意参与到整个的原生app与PWA的争论中,但是有一件事是确定的– 在提高移动端和改善用户体验方面还有很长的路要走。到2018,移动端web访问量注定会超过其他设备的总和,你能忽视这一趋势吗?
好消息是制作一个PWA并不困难。实际上,很多可能可以将一个现有的网站转换成PWA。这也是本文将要介绍的内容– 看完本文,你将拥有一个行为类似于原生web app,能够离线运行,拥有自己的桌面图标。
什么是PWA?
增强型网页应用(Progressive Web Apps,PWAs),是web技术里一个令人激动的革新。PWAs集合了一系列技术,可以使web app有和原生移动app相同的功能。这有利于开发者和用户实现那些仅限于web和native的解决方案:
你只需要一个由开源的、标准的W3C网页技术开发的应用。无需单独开发一个原生代码库。
用户可以发现并在下载前尝试你的应用。
没有必要使用需要支付费用的应用商店,应用程序会自动更新,而用户无感知。
下载前有提示,下载后会在主屏幕上添加图标。
启动时,PWA会展示一个聚睛的启动画面。
chrome浏览器选项可以设置全屏显示。
基本文件会被缓存到本地,因此PWAs比标准的网页app响应地更快(甚至比原生app还要快)。
安装包非常轻量– 可能只有几百KB的缓存数据。
所有的数据交换只能通过安全的HTTPS连接。
PWAs的功能是离线的,并能在连接响应后同步数据。
现在还是早期,但对于案例的研究还是积极的。Flipkart,印度最大的电商网站,在弃用原生app改用PWA后,销售转换率增长了70%,用户的停留时间变成了三倍。阿里巴巴,世界最大的商业交易平台,转换率也差不多增长了76%。
Firefox、Chrome和其他基于Blink的浏览器都是支持PWA技术的。微软正致力于在Edge中实现。Apple保持了沉默,尽管在WebKit的五年计划中有一些有希望的评论。幸运地是,这与浏览器端的支持性几乎不相干。
PWA是渐进增强的
你的app仍然可以在不支持PWA的浏览器中的运行,只是用户无法享受到离线功能,但一切都会如常运行。在给出效益成本比回报的情况下,没有理由不往你的系统里添加PWA技术。
不仅仅是App
谷歌在推广PWA时给的大部分教程都是描述如何从头建立一个基于Chrome而似native的移动app。然而,我们不需要多加一个单页面app或非得去跟从实际的界面设计指南。大多数的网站可以在几个小时内改造成PWA,包括WordPress可静态网站。Smashing Magazine宣布他们也在使用PWA。
演示代码
演示代码可参考https://github.com/sitepoint-editors/pwa-retrofit,这是一个简单的4页网站,有一些图片、一个样式表和一个主js文件。这个网站兼容所有现在浏览器(IE10+)。如果浏览器支持PWA,那么用户可以在离线环境下阅读曾经浏览过的网页。
运行代码时,请确保已经安装了Node.js,之后可以在终端运行下面的代码启动web server:
node./server.js [port][port]是配的,默认值8888。打开Chrome或其他基于Blink的浏览器(比如Opera或Vivaldi),然后打开 http://localhost:8888/(端口可改成你所指定的值)。你也可以打开开发者工具(F12或Cmd/Ctrl + Shift + I)查看控制台信息。
浏览一下主页,和其他页面,然后通过以下两种方式,切换成离线状态。
使用Cmd/Ctrl + C停止web server,
在控制台的Network或Application– Service Workers 选项卡,选中离线选项。
重新访问之前浏览过的页面,发现它们仍能加载。如果访问一个从未浏览过的页面,将会展示一个‘you’re offline’页面,并包含一个可浏览页面的列表:
连接设备
你也可以在Android机上浏览演示代码,手机需要通过USB连接到PC或Mac上。打开开发者工具左上角三点处的More tools菜单里的Remote devices面板。
选择左侧的Settings,点击Add Rule将端口8888映射到localhost:8888。现在你可以打开手机上的浏览器并访问http://localhost:8888/
有两种方式向桌面添加图标,你可以使用浏览器菜单中的“Add to Home screen”,也可以浏览一些页面后,浏览器会提示你下载。浏览完页面后,关闭Chrome并断开设备连接。然后再次启动PWA Website应用 – 你会看到一个启动画面,也能够在不连接服务器的情况下,浏览之前阅读过的页面。
下面介绍如何三步将你的网站改造成一个PWA。
Step 1:开启HTTPS
显然,PWAs需要HTTPS连接。虽然成本会增加,但这些成本和代价是值得的,毕竟谷歌搜索会将安全的网站排名更高。
HTTPS对于上边的演示代码是非必需的,因为Chrome允许使用localhost或任何127.x.x.x地址进行测试。如果想在HTTP网站上测试PWA技术,在启动Chrome时可以使用下面的命令行标记:
·--user-data-dir
·--unsafety-treat-insecure-origin-as-secure
Step 2:创建Web App Manifest
Webapp manifest包括应用程序信息,比如名称、描述和操作系统桌面图标、启动画面和视口的图片。事实上,manifest用一个单独文件替代了页面中已存在的大量的特定图标和主题meta标签。
Manifest是JSON文件,位于app根目录下。它的Content-Type需要设置为application/manifest+json 或 application/json。这个文件的名称可随意更改,但在该演示代码中被命名为/manifest.json。
{
"name" :"PWAWebsite",
"short_name" :"PWA",
"description" :"Anexample 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"
}
]
}
需要在所有页面中添加如下链接:
<link rel="manifest"href="/manifest.json">
Manifest中的主要属性有:
name – 展示给用户的应用程序的全称
short_name –全称无法完全显示时使用的短名称
description– 对于应用的详细描述
start_url –启动程序对应的URL(一般是‘/’)
scope – 导航范围。例如‘/app/’指限制应用只访问该文件夹
background-color– 启动界面和chrome浏览器(如果需要的话)的背景颜色
theme_color– 应用程序的颜色,通常与背景色相同,它将影响app的显示效果
orientation– 设置优先的显示方向:any, natural, landscape, landscape-primary, landscape-secondary,portrait, portrait-primary,和 portrait-secondary
display – 设置优先的显示模式:fullscreen (非chrome), standalone (看上去像一个原生app), minimal-ui (包含一小部分UI控件) and browser (一个常规的浏览器标签页)
icons – 一个图片对象列表,定义了src(地址)、sizes(宽高)和type(类型)。有一系统的图标应该被定义。
MDN提供了完整的属性列表(https://developer.mozilla.org/en-US/docs/Web/Manifest)
Chrome开发者工具Application面板中的Manifest选项,显示了应用的manifest JSON,还提供了“Add to homescreen”链接,可以在桌面设备上运行:
Step 3:创建Service Worker
SeviceWorker 是可编程的代理,能够拦截并响应网络请求。它是位于应用根目录下的一个JavaScript文件。
页面js(在该演示代码中是/js/main.js)中可以检查是否支持serviceworker并注册该文件:
if('serviceWorker'in navigator){
//register service worker
navigator.serviceWorker.register('/service-worker.js');
}
如果你不需要离线功能,那么只需要创建一个空白的/service-worker.js文件 – 用户将被提示安装当前应用。
serviceworker会有些难懂,但你应该去改写演示代码以满足你的需要。这是标准的web worker脚本,浏览器会下载(如果支持的话)该脚本并运行在独立的线程中。它不能访问DOM和其他页面的API,但会拦截各种网络请求,包括由页面改变、资源下载和Ajax调用所触发的。
这是网站需要使用HTTPS的首要原因。试想一下,如果一个第三方脚本从另外的域名注入了其自己的sevice worker,将会造成怎样的状况。它将可以检测并修改客户端和服务器之间的所有数据交换!
serviceworker会对3种主要事件作出反应:install、activate和fetch。
Install事件
该事件发生在应用安装时。通常是使用Cache API来缓存基本文件。
首先,我们将定义一些配置项:
缓存名称(CACHE)和版本号(version)。应用可以有多个缓存区,但我们只需要一个。版本号可以保证,当我们大幅修改应用后,本地能够使用新的缓存并忽视原有的缓存文件。
一个离线页面URL(offlineURL)。当离线用户尝试加载一个从未访问过的页面时,将显示该离线页面。
一个必要文件数组,包含了网站离线运行必要的文件(installFilesEssential)。数组里还应包括资源文件,如CSS和JavaScript;我还会加上主页(/)和logo。如果某些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 assetsfunctioninstallStaticFiles(){
returncaches.open(CACHE)
.then(cache=>{
// cache desirable filescache.addAll(installFilesDesirable);
// cache essential filesreturncache.addAll(installFilesEssential);
});}最后,我们添加一个install事件监听器。waitUntil方法确保serviceworker在所有封闭代码都已运行后才被安装,方法中运行installStaticFiles() 后运行 self.skipWaiting() ,激活service worker:
// application installationself.addEventListener('install',event=>{
console.log('service worker: install'); // cache core files event.waitUntil( installStaticFiles().then(()=>self.skipWaiting())
);});Activate事件
该事件发生在serviceworker被激活时,不论是安装后还是返回时。你可以用不到这一句柄,但演示代码中使用了该事件,用于删除现有的旧缓存:
// clear old cachesfunctionclearOldCaches(){returncaches.keys()
.then(keylist=>{
returnPromise.all(
keylist.filter(key=>key!==CACHE)
.map(key=>caches.delete(key))
); });}// application activatedself.addEventListener('activate',event=>{
console.log('service worker: activate'); // delete old caches event.waitUntil( clearOldCaches().then(()=>self.clients.claim())
);});Fetch事件
该事件发生在网络请求产生时。它会调用respondWith()方法来劫持GET请求并:
从缓存中获取资源,
如果#1失败,使用Fetch API(与fetch事件无关)从网络上加载资源,并将该资源加进缓存,
如果#1和#2都失败了,返回一个适当的结果。
// application fetch network dataself.addEventListener('fetch',event=>{
// abandon non-GET requestsif(event.request.method!=='GET')return;
leturl=event.request.url;
event.respondWith(caches.open(CACHE)
.then(cache=>{
returncache.match(event.request)
.then(response=>{
if(response){
// return cached fileconsole.log('cache fetch: '+url);
returnresponse;
} // make network requestreturnfetch(event.request)
.then(newreq=>{
console.log('network fetch: '+url);
if(newreq.ok)cache.put(event.request,newreq.clone());
returnnewreq;
}) // app is offline.catch(()=>offlineAsset(url));
}); }) ); });最后调用的offlineAsset(url)方法返回了一个适当的结果,提供了一些帮助功能:
// is image URL?letiExt=['png','jpg','jpeg','gif','webp','bmp'].map(f=>'.'+f);
functionisImage(url){returniExt.reduce((ret,ext)=>ret||url.endsWith(ext),false);
}// return offline assetfunctionofflineAsset(url){ if(isImage(url)){
// return image returnnewResponse( '<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 pagereturncaches.match(offlineURL);
} }offlineAsset(url)函数会检查该request是否请求的是一张图片,是的话返回一个包含“offline”文字的SVG,不是的话,返回offlineURL页面。
Chrome开发者工具Application面板中的ServiceWorker选项,提供了有关worker的信息,
在开发者工具的 Cache Storage 选项列出了所有当前域内的缓存和所包含的静态文件。当缓存更新的时候,你可以点击左下角的刷新按钮来更新缓存:
不出意料, Clear storage 选项可以删除你的 serviceworker 和缓存:
再来一步 – Step 4:创建一个可用的离线页面
离线页面可以是一个静态页面,来说明当前用户请求不可用。然而,我们也可以在这个页面上列出可以访问的页面链接。
在main.js中我们可以使用 Cache API 。然而API 使用promises,在不支持的浏览器中会引起所有javascript运行阻塞。为了避免这种情况,我们在加载另一个 /js/offlinepage.js 文件之前必须检查离线文件列表和是否支持 Cache API 。
// load script to populate offline page listif(document.getElementById('cachedpagelist') &&'caches'inwindow) {
varscr =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 节点中:
ent('li'),
// cache nameconstCACHE ='::PWAsite',
offlineURL ='/offline/',
list =document.getElementById('cachedpagelist');
// fetch all cacheswindow.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 cachecaches.open(cacheList[0])
.then(cache => { // fetch cached pages cache.keys() .then(reqList => {letfrag =document.createDocumentFragment();
reqList .map(req => req.url).filter(req => (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL))
.sort()
.forEach(req => { letli =document.createElem 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=86400PS: 备份内容仅显示纯文字。