有关将网站改造为渐进式Web应用程序的文章包含在我们的选集Modern JavaScript中 。 如果您希望所有内容都集中在一个地方,以加快使用现代JavaScript的速度,请注册SitePoint Premium并下载一个副本。
最近,有关渐进式Web应用程序(PWA)的讨论越来越多,许多人质疑它们是否代表了(移动)网络的未来。
我不会涉及整个本机应用程序与PWA的争论,但可以肯定的是:它们在增强移动性和改善其用户体验方面还有很长的路要走。
随着移动Web的访问量注定要超过所有其他设备的访问量,您是否可以忽略这一趋势?
好消息是制作PWA并不困难。 实际上,可以将现有网站转换为PWA。 这正是我在本教程中要做的。 待您完成时,您将拥有一个行为类似于本地Web应用程序的网站。 它可以脱机工作,并具有自己的主屏幕图标。
渐进式Web应用程序(简称PWA )是Web技术中令人兴奋的创新。 PWA包含多种技术,可以使Web应用程序像本地移动应用程序一样运行。 为开发人员和用户带来的好处克服了纯Web和纯本机解决方案带来的限制:
现在还处于初期,但案例研究是积极的 。 印度最大的电子商务网站Flipkart在放弃本地应用程序进行PWA时,销售转化率提高了70%,现场时间增加了三倍 。 世界上最大的商业交易平台阿里巴巴的转换率也增长了76% 。
Firefox,Chrome和其他基于Blink的浏览器均提供了可靠的PWA技术支持。 Microsoft正在开发Edge实施。 尽管WebKit五年计划中有令人鼓舞的评论,苹果仍然保持沉默。 幸运的是,浏览器支持基本上无关紧要……
您的应用仍将在不支持PWA技术的浏览器中运行。 用户将无法获得脱机功能的好处,但是一切将继续像以前一样工作。 考虑到成本效益的回报,没有理由不将PWA技术添加到您的系统中。
Google领导了PWA运动 ,因此大多数教程都从头开始描述如何构建基于Chrome的本机外观移动应用程序。 但是,您不需要特殊的单页应用程序,也不必遵循重要的界面设计准则。 大多数网站可以在几个小时内实现PWA认证。 其中包括您的WordPress或静态站点。
演示代码可从GitHub获得 。
它提供了一个简单的四页网站,其中包含一些图像,一个样式表和一个主JavaScript文件。 该网站可在所有现代浏览器(IE10 +)中使用。 如果浏览器支持PWA技术,则用户可以在脱机时阅读以前查看的页面。
要运行代码,请确保已安装Node.js ,然后使用以下命令在终端中启动提供的Web服务器:
node ./server.js [port]
在上面的代码中, [port]
是可选的,默认为8888。打开Chrome或其他基于Blink的浏览器,例如Opera或Vivaldi,然后导航到http:// localhost:8888 / (或您指定的任何端口)。 您也可以打开开发人员工具(F12或Cmd / Ctrl + Shift + I )以查看各种控制台消息。
查看主页,或者查看另一个页面,然后通过以下任一方法使其离线:
重新访问您先前查看的任何页面,它们仍将加载。 访问未显示有“您处于离线状态”页面的页面,该页面包含可见页面列表:
您也可以在通过USB连接到PC / Mac的Android智能手机上查看演示页面。 从左上角三点菜单中的“ 更多工具”打开“ 远程设备”面板。
选择左侧的设置 ,然后单击添加规则以将端口8888转发到localhost:8888。 现在,您可以在智能手机上打开Chrome并导航到http:// localhost:8888 / 。
您可以使用浏览器菜单来“添加到主屏幕”。 进行两次访问,浏览器将提示您“安装”。 这两个选项都会在主屏幕上创建一个新图标。 浏览几页,然后关闭Chrome并断开设备连接。 然后,您可以启动PWA网站应用程序。 尽管没有连接到服务器,您仍将看到一个初始屏幕并能够查看以前阅读的页面。
只需三个基本步骤即可将您的网站转换为渐进式Web应用程序……
PWA需要HTTPS连接,原因很快就会显现出来。 价格和流程因主机而异,但值得付出的成本和精力,因为Google搜索对安全网站的排名更高。
上面的演示不需要HTTPS,因为Chrome允许使用localhost或任何127.xxx地址进行测试。 如果您使用以下命令行标志启动Chrome,也可以在HTTP网站上测试PWA技术:
--user-data-dir
--unsafety-treat-insecure-origin-as-secure
Web应用程序清单提供了有关应用程序的信息,例如名称,描述和图像,操作系统使用它们来配置主屏幕图标,初始页面和视口。 本质上,清单是单个文件的替代品,可以替代您可能已经在页面中包含的大量特定于供应商的图标和主题元标记。
清单是应用程序根目录中的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"
}
]
}
您所有页面的中都需要此文件的链接:
主要清单属性是:
/
) /app/
的范围会将应用程序限制为该文件夹 any
, natural
, landscape
, landscape-primary
landscape-secondary
, portrait
, portrait-primary
portrait-secondary
fullscreen
(无chrome), standalone
(看起来像本机应用程序), minimal-ui
(一minimal-ui
UI控件)和browser
(传统的浏览器标签) src
URL, sizes
和type
(应定义图标范围)的图像对象数组。 MDN提供了Web App Manifest属性的完整列表。
Chrome的“开发工具”的“ 应用程序”选项卡的“清单”部分验证您的清单JSON并提供“添加到主屏幕”链接,该链接可在桌面设备上运行:
服务人员是可编程的代理,可以拦截和响应网络请求。 它们是驻留在应用程序根目录中的单个JavaScript文件。
您的页面JavaScript(在演示代码中为/js/main.js
)可以检查对Service Worker的支持并注册文件:
if ('serviceWorker' in navigator) {
// register service worker
navigator.serviceWorker.register('/service-worker.js');
}
如果不需要脱机功能,只需创建一个空的/service-worker.js
文件。 将提示用户安装您的应用程序!
服务人员可能会感到困惑,但是您应该能够根据自己的目的调整演示代码。 这是浏览器下载(如果可能)并在单独的线程上运行的标准Web工作者脚本。 它无权访问DOM或其他页面API,但将拦截由页面更改,资产下载和Ajax调用触发的网络请求。
这是您的站点需要HTTPS的主要原因。 想象一下,如果第三方脚本可以从另一个域注入其自己的服务工作者,那么该混乱情况。 它将能够检查和修改客户端与服务器之间的所有数据交换!
服务人员对三个主要事件做出反应: install
, activate
和fetch
。
在安装应用程序时会发生这种情况。 它通常用于使用Cache API缓存基本文件。
首先,我们将为以下定义一些配置变量:
CACHE
)和版本( version
)。 您的应用程序可以有多个缓存存储,但是我们只需要一个。 版本号已应用,因此,如果我们进行了重大更改,将使用新的缓存,而所有先前缓存的文件将被忽略。 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
方法可确保在执行所有包含的代码之前,不会安装服务工作者。 它先运行installStaticFiles()
然后运行self.skipWaiting()
以使服务工作者处于活动状态:
// application installation
self.addEventListener('install', event => {
console.log('service worker: install');
// cache core files
event.waitUntil(
installStaticFiles()
.then(() => self.skipWaiting())
);
});
在安装后或返回时立即激活Service Worker时,会发生这种情况。 您可能不需要此处理程序,但是演示代码使用一个来删除旧的高速缓存(如果存在):
// 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()
调用将此服务工作程序设置为站点的活动工作程序。
每当发出网络请求时,就会发生这种情况。 它调用respondWith()
方法劫持GET请求并返回:
// 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(
'',
{ headers: {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'no-store'
}}
);
}
else {
// return page
return caches.match(offlineURL);
}
}
offlineAsset()
函数检查请求是否针对图像,并返回包含文本“ offline”的SVG。 所有其他请求都返回offlineURL
页面。
Chrome的“开发工具”的“ 应用程序”选项卡的“ 服务工作者”部分提供了有关您的工作者的信息,并包含错误和设施,这些错误和设施会强制重新加载并使浏览器离线:
缓存存储部分列出了当前范围内的所有缓存及其包含的缓存资产。 缓存更新后,您可能需要单击刷新按钮:
毫不奇怪,“ 清除存储”部分可以删除服务工作者和缓存:
离线页面可以是静态HTML,通知用户他们请求的页面无法离线使用。 但是,我们还可以提供可供阅读的页面URL列表。
可以在我们的main.js
脚本中访问Cache API 。 但是,API使用的Promise会在不受支持的浏览器中失败,并会导致所有JavaScript停止执行。 为避免这种情况,我们将添加代码,以在加载另一个/js/offlinepage.js
JavaScript文件(该文件必须存在于上述installFilesEssential
数组中)之前检查脱机列表元素和Caches 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
通过版本名称查找最新的缓存,获取所有URL密钥的列表,删除非页面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调试很困难,那么服务人员就不会很有趣! 开发人员工具的Chrome的“ 应用程序”标签提供了一组可靠的功能,日志记录语句也输出到控制台。
在开发过程中,您应该考虑在隐身窗口中运行您的应用程序,因为关闭选项卡后不会保留缓存的文件。
Firefox提供了一个JavaScript调试器,可通过工具菜单的Service Workers选项进行访问。 承诺很快会提供更好的设施。
最后, 用于Chrome的Lighthouse扩展还提供了有关PWA实施的有用信息。
渐进式Web应用程序需要新技术,因此建议谨慎使用。 就是说,它们是对您现有网站的增强,该过程应该不会超过几个小时,并且不会对不支持的浏览器产生负面影响。
开发人员的意见各不相同,但有几点要考虑……
该演示站点将隐藏URL栏,除非您具有游戏等单一URL应用程序,否则我不建议这样做。 清单选项display: minimal-ui
或display: browser
对于大多数站点, display: browser
可能是最好的。
您可以缓存网站上的每个页面和资产。 这对于小型网站很好,但对于具有数千个页面的网站来说是否可行? 没有人可能对您的所有内容都感兴趣,并且可能会超出设备存储限制。 即使仅像演示一样存储访问的页面和资产,缓存也可能过度增长。
也许考虑:
该演示在从网络加载之前在高速缓存中查找资产。 当用户离线时,这很棒,但意味着即使他们在线也可以查看旧页面。
诸如图片和视频之类的资产的URL永远都不应更改,因此长期缓存几乎不是问题。 您可以使用Cache-Control
HTTP标头确保它们保留至少一年(31,536,000秒)的Cache-Control
:
Cache-Control: max-age=31536000
页面,CSS和脚本文件的更改频率可能更高,因此您可以将有效期设置为较短的24小时,并确保在线时针对服务器版本对它进行了验证:
Cache-Control: must-revalidate, max-age=86400
您还可以考虑使用缓存清除技术来确保无法使用较旧的资产,例如,命名CSS文件styles-abc123.css
并在每个发行版上更改哈希。
缓存可能会变得很复杂,因此,我建议您阅读Jake Archibold的“ 缓存最佳实践”和“最大寿命”。
如果您想了解有关渐进式Web应用程序的更多信息,以下资源很有用:
网上也有许多文章影响了我处理此演示代码的方式。 随时修改代码,让我知道它的运行方式。 祝你好运!
本文由AJ Latour , Panayiotis«pvgr»Velisarakos和Dave Maxwell进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
From: https://www.sitepoint.com/retrofit-your-website-as-a-progressive-web-app/