PWA(Progressive Web App,渐进式Web应用),是近年来Web开发领域最热门的技术之一。
它由Google在2015年提出,核心思想是:让Web应用拥有接近原生应用的体验。PWA应用,可以离线访问,可以添加到手机桌面,可以推送通知,可以全屏运行,可以在弱网环境下快速加载。它兼具了Web应用的易传播、免安装、跨平台,和原生应用的高性能、好体验、可离线的优点。
2018年,Chrome、Firefox、Safari等主流浏览器,都已经支持PWA的核心特性。越来越多的公司,开始把自己的Web应用改造成PWA,比如Twitter、阿里巴巴、京东、星巴克等。PWA,已经从一个概念,变成了一个可以在生产环境中使用的成熟技术。
今天,我想从基础到高级,详细讲解一下PWA的配置和实现,包括PWA的核心概念、Web App Manifest、Service Worker、缓存策略、推送通知、添加到桌面、以及一些高级技巧和最佳实践。
一、PWA的核心概念
在开始配置之前,先搞清楚PWA的几个核心概念。
什么是PWA?
PWA,全称Progressive Web App,中文叫渐进式Web应用。它不是一个单一的技术,而是一系列Web技术的集合,通过这些技术,让Web应用拥有接近原生应用的体验。
PWA的核心特性包括:
- 可靠(Reliable):在弱网或者离线环境下,也能快速加载,不会显示白屏。
- 快速(Fast):页面加载速度快,交互流畅,用户操作响应迅速。
- 沉浸式(Engaging):可以添加到桌面,全屏运行,推送通知,让用户感觉像是在用原生应用。
PWA的三大支柱:
PWA的实现,主要依赖三个核心技术:
- Web App Manifest:一个JSON配置文件,定义了应用的名称、图标、主题色、启动方式等信息,让应用可以添加到桌面。
- Service Worker:一个运行在浏览器后台的脚本,独立于网页,可以拦截请求、缓存资源、处理推送通知,是实现离线访问和推送通知的核心。
- HTTPS:PWA必须运行在HTTPS环境下(localhost除外),这是为了安全,防止中间人攻击。
只要你的网站满足这三个条件,就可以说是一个PWA了。当然,还有一些其他的特性,比如响应式设计、应用外壳架构、推送通知等,可以让你的PWA体验更好。
PWA的优势:
相比传统的Web应用和原生应用,PWA有以下优势:
- 免安装:不需要去应用商店下载安装,用户访问网站就可以使用,也可以一键添加到桌面。
- 跨平台:一套代码,可以在所有支持PWA的浏览器和平台上运行,不需要为iOS、Android分别开发。
- 易传播:通过URL就可以分享,用户点击链接就可以访问,传播成本低。
- 省空间:不需要下载安装包,不占用手机存储空间,缓存的数据也很小。
- 自动更新:每次访问都是最新版本,不需要用户手动更新,也不需要应用商店审核。
- 离线可用:通过Service Worker缓存,可以在离线或者弱网环境下使用。
当然,PWA也不是完美的,它也有一些局限性,比如对硬件的访问能力不如原生应用,iOS上的支持还不够完善,推送通知的支持在不同平台上有差异等。但是,随着技术的发展,这些问题正在逐步解决。
二、第一步:配置Web App Manifest
Web App Manifest,是PWA的第一步。它是一个JSON文件,定义了应用的基本信息,让浏览器知道你的应用可以添加到桌面,以及添加到桌面之后的显示方式。
创建manifest.json:
在你的网站根目录下,创建一个manifest.json文件,内容如下:
{
"name": "我的PWA应用",
"short_name": "PWA",
"description": "这是一个PWA示例应用",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4285f4",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}字段说明:
- name:应用的完整名称,会显示在安装弹窗和应用列表中。
- short_name:应用的短名称,添加到桌面后,图标下面显示的名称,建议不超过12个字符。
- description:应用的描述,说明应用是做什么的。
- start_url:应用启动时打开的URL,可以是相对路径,也可以是绝对路径。
- display:应用的显示模式,有四个可选值:
- fullscreen:全屏模式,占用整个屏幕,隐藏浏览器UI。 - standalone:独立应用模式,看起来像原生应用,有自己的窗口,隐藏浏览器的地址栏和工具栏。 - minimal-ui:最小UI模式,比standalone多一些最小的浏览器UI元素。 - browser:浏览器模式,和普通网页一样。 一般推荐用standalone,体验最接近原生应用。
- background_color:应用启动时的背景色,在应用加载完成之前显示,建议和页面的背景色一致,避免闪屏。
- theme_color:应用的主题色,会影响浏览器的地址栏颜色、任务切换时的颜色等。
- orientation:应用的屏幕方向,可选值有
any、natural、landscape、portrait等,一般移动端应用用portrait-primary(竖屏)。 - icons:应用的图标数组,至少需要192x192和512x512两个尺寸的图标。图标格式推荐PNG,也支持SVG和WebP。
在HTML中引入manifest:
创建好manifest.json之后,需要在你的HTML页面中,通过<link>标签引入:
<link rel="manifest" href="/manifest.json">把这行代码放在<head>标签里,就可以了。
验证manifest:
配置好之后,可以在Chrome浏览器中,打开开发者工具,切换到Application面板,点击Manifest,就可以看到你的manifest配置是否正确,有没有问题。
三、第二步:注册和使用Service Worker
Service Worker,是PWA的核心,也是最复杂的部分。它是一个运行在浏览器后台的脚本,独立于网页,可以拦截网络请求、缓存资源、处理推送通知,是实现离线访问和推送通知的关键。
Service Worker的特点:
- 运行在独立的线程:Service Worker运行在独立的线程中,不阻塞主线程,不影响页面的性能。
- 没有DOM访问权限:Service Worker不能直接访问页面的DOM,它通过postMessage和页面通信。
- 事件驱动:Service Worker是事件驱动的,它会在需要的时候被唤醒,处理完事件后自动休眠,不会一直占用资源。
- 可以拦截请求:Service Worker可以拦截页面发出的所有网络请求,决定是从缓存取,还是从网络取,还是自定义响应。
- 必须在HTTPS下运行:为了安全,Service Worker必须在HTTPS环境下运行(localhost除外,方便开发调试)。
注册Service Worker:
首先,在你的网站根目录下,创建一个sw.js文件(Service Worker脚本)。然后,在你的HTML页面中,注册这个Service Worker:
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('Service Worker 注册成功,作用域是:', registration.scope);
})
.catch(function(error) {
console.log('Service Worker 注册失败:', error);
});
});
}
</script>这段代码的意思是:如果浏览器支持Service Worker,就在页面加载完成之后,注册根目录下的sw.js。注册成功后,Service Worker的作用域默认是它所在的路径,也就是根目录下的所有页面,都会被这个Service Worker控制。
注意:Service Worker的注册路径,决定了它的作用域。sw.js放在根目录,作用域就是整个网站;如果放在/js/目录下,作用域就只有/js/路径下的页面。所以,一般把sw.js放在根目录。
Service Worker的生命周期:
Service Worker有自己的生命周期,了解生命周期,是正确使用Service Worker的关键。
- 解析(Parsed):浏览器解析sw.js文件,如果解析成功,就进入安装阶段。
- 安装(Installing):Service Worker第一次注册,或者有新版本的时候,会进入安装阶段。在这个阶段,可以做一些初始化的工作,比如缓存静态资源。安装完成后,进入等待激活阶段。
- 等待激活(Waiting):如果页面上已经有一个旧的Service Worker在运行,新的Service Worker安装完成后,会进入等待状态,直到所有的页面都关闭,旧的Service Worker被卸载,新的才会激活。
- 激活(Activating):Service Worker激活的时候,可以做一些清理工作,比如删除旧版本的缓存。激活完成后,Service Worker就开始工作了,可以拦截请求、处理事件了。
- 激活(Activated):Service Worker完全激活,开始控制页面,处理各种事件。
- 冗余(Redundant):如果安装失败,或者被新的版本替换,Service Worker就会变成冗余状态,被浏览器回收。
一个最简单的Service Worker:
下面是一个最简单的Service Worker示例,实现了静态资源的缓存:
// 缓存名称,包含版本号,方便更新
const CACHE_NAME = 'my-pwa-v1';
// 需要缓存的静态资源列表
const urlsToCache = [
'/',
'/index.html',
'/styles/main.css',
'/script/main.js',
'/images/logo.png'
];
// 安装事件:缓存静态资源
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('缓存静态资源');
return cache.addAll(urlsToCache);
})
);
});
// 激活事件:清理旧缓存
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
return cacheName !== CACHE_NAME;
}).map(function(cacheName) {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
})
);
})
);
});
// 请求拦截事件:缓存优先策略
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 如果缓存中有,就返回缓存
if (response) {
return response;
}
// 缓存中没有,就从网络获取
return fetch(event.request);
})
);
});这个Service Worker,做了三件事:
- 安装的时候,把指定的静态资源缓存起来。
- 激活的时候,删除旧版本的缓存。
- 拦截所有请求,如果缓存中有,就返回缓存;没有,就从网络获取。
这样,即使离线了,缓存的静态资源也能正常访问,实现了离线可用。
四、缓存策略
Service Worker最核心的功能,就是拦截请求和缓存资源。不同的资源,适合不同的缓存策略。下面介绍几种常用的缓存策略:
1. 缓存优先(Cache First)
缓存优先,就是先从缓存取,如果缓存中有,就直接返回缓存;如果缓存中没有,再从网络获取,并且把获取到的结果存入缓存。
这种策略,适合不经常变化的静态资源,比如CSS、JS、图片、字体等。这些资源,加载一次之后,就可以一直用缓存,不需要每次都从网络获取,加载速度快,也能离线访问。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request).then(function(response) {
// 把网络获取的结果存入缓存
return caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});2. 网络优先(Network First)
网络优先,就是先从网络获取,如果网络成功,就返回网络结果,并且更新缓存;如果网络失败(比如离线),就从缓存取。
这种策略,适合经常变化的资源,比如API接口数据、新闻列表等。这些资源,需要优先获取最新的内容,但是在离线的时候,也能显示缓存的旧内容。
self.addEventListener('fetch', function(event) {
event.respondWith(
fetch(event.request)
.then(function(response) {
// 网络成功,更新缓存
return caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
})
.catch(function() {
// 网络失败,从缓存取
return caches.match(event.request);
})
);
});3. stale-while-revalidate(缓存同时更新)
这种策略,是先返回缓存的内容,让用户快速看到页面;同时,在后台从网络获取最新内容,更新缓存。下一次访问的时候,就能看到最新的内容了。
这种策略,兼顾了加载速度和内容新鲜度,适合那些对实时性要求不是特别高,但是又希望内容能更新的资源,比如文章列表、商品列表等。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return cache.match(event.request).then(function(cachedResponse) {
// 从网络获取最新内容,更新缓存
var fetchPromise = fetch(event.request).then(function(networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// 先返回缓存,同时后台更新
return cachedResponse || fetchPromise;
});
})
);
});4. 仅网络(Network Only)
仅网络,就是只从网络获取,不使用缓存。这种策略,适合那些必须实时获取、不能缓存的资源,比如支付接口、实时数据等。
self.addEventListener('fetch', function(event) {
event.respondWith(fetch(event.request));
});5. 仅缓存(Cache Only)
仅缓存,就是只从缓存取,不从网络获取。这种策略,适合那些在安装的时候就缓存好了、永远不会变化的资源。
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request));
});实际应用中的策略选择:
在实际应用中,一般不会只用一种策略,而是根据不同的资源类型,使用不同的策略。比如:
- 静态资源(CSS、JS、图片、字体):缓存优先。
- API接口数据:网络优先,或者stale-while-revalidate。
- HTML页面:stale-while-revalidate,或者网络优先。
- 支付、实时数据:仅网络。
可以在Service Worker的fetch事件中,根据请求的URL、类型等,判断使用哪种策略。
五、推送通知
推送通知,是PWA的另一个重要特性,它让Web应用可以像原生应用一样,向用户推送通知,即使用户没有打开页面。
推送通知,需要两个API配合:
- Notification API:用于显示通知。
- Push API:用于从服务器推送消息到客户端。
请求通知权限:
在使用推送通知之前,需要先请求用户的权限。在页面中,调用以下代码:
if ('Notification' in window) {
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('用户同意了通知权限');
} else {
console.log('用户拒绝了通知权限');
}
});
}显示一个简单的通知:
有了权限之后,就可以显示通知了。最简单的方式,是在页面中直接调用:
if (Notification.permission === 'granted') {
new Notification('通知标题', {
body: '这是通知的内容',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png'
});
}但是,这种方式,只有在页面打开的时候才能显示通知。如果页面关闭了,就收不到了。要实现页面关闭也能收到通知,需要用Push API,配合Service Worker。
服务端推送(Push API):
服务端推送的实现,比较复杂,大致流程是:
- 客户端(浏览器)订阅推送服务,获取一个endpoint(推送地址)。
- 客户端把这个endpoint发送给你的服务器,保存起来。
- 你的服务器,在需要推送的时候,向这个endpoint发送消息。
- 浏览器的推送服务,收到消息后,唤醒对应的Service Worker。
- Service Worker收到push事件,显示通知。
客户端订阅推送的代码:
// 在页面中,注册Service Worker之后
navigator.serviceWorker.ready.then(function(registration) {
return registration.pushManager.subscribe({
userVisibleOnly: true, // 推送的消息必须对用户可见
applicationServerKey: urlBase64ToUint8Array('你的公钥')
});
}).then(function(subscription) {
// 把subscription发送给你的服务器保存
fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
});Service Worker中处理push事件:
self.addEventListener('push', function(event) {
var data = event.data.json();
var title = data.title;
var options = {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
data: data
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// 处理通知点击事件
self.addEventListener('notificationclick', function(event) {
event.notification.close();
// 点击通知后,打开对应的页面
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});服务端推送的实现,需要用到Web Push协议,不同的语言有不同的库。Node.js可以用web-push库,PHP可以用minishlink/web-push库,Python可以用pywebpush库。
推送通知,是PWA中比较复杂的部分,但是也是最能提升用户体验的部分。如果你的应用需要推送通知,建议花时间研究一下。
六、添加到桌面和安装提示
当你的网站配置了manifest.json和Service Worker,并且满足一定条件的时候,浏览器会自动提示用户,是否要把你的应用添加到桌面。
Chrome的自动安装提示,需要满足以下条件:
- 有有效的manifest.json,包含name、shortname、starturl、display(standalone或fullscreen)、icons(至少192和512)。
- 有注册并激活的Service Worker。
- 网站运行在HTTPS下。
- 用户和网站有过交互(至少有一次点击或按键)。
- 同一个域名,每90天最多提示一次。
满足条件后,Chrome会在页面加载后,弹出一个安装提示栏,问用户是否要添加到桌面。
自定义安装提示:
默认的安装提示,样式是固定的,不能自定义。但是,你可以监听beforeinstallprompt事件,阻止默认的提示,然后用你自己的UI来提示用户安装。
let deferredPrompt;
window.addEventListener('beforeinstallprompt', function(e) {
// 阻止默认的安装提示
e.preventDefault();
// 保存事件对象,后面用
deferredPrompt = e;
// 显示你自己的安装按钮
showInstallButton();
});
// 用户点击你的安装按钮时
function installApp() {
if (deferredPrompt) {
// 显示浏览器的安装弹窗
deferredPrompt.prompt();
// 等待用户的选择
deferredPrompt.userChoice.then(function(choiceResult) {
if (choiceResult.outcome === 'accepted') {
console.log('用户同意安装');
} else {
console.log('用户拒绝安装');
}
deferredPrompt = null;
});
}
}
// 监听安装成功事件
window.addEventListener('appinstalled', function(e) {
console.log('应用安装成功');
});通过自定义安装提示,你可以在你认为合适的时机(比如用户完成了一个操作、体验了核心功能之后),提示用户安装,转化率会更高。
七、PWA的调试和验证
开发PWA的时候,Chrome的开发者工具,提供了很好的调试支持。
Application面板:
打开Chrome开发者工具,切换到Application面板,这里可以看到和PWA相关的所有信息:
- Manifest:查看manifest.json的配置是否正确,图标、名称、主题色等是否正常。
- Service Workers:查看当前页面注册的Service Worker,包括状态、作用域、脚本地址等。可以手动更新、注销Service Worker,也可以模拟离线。
- Cache Storage:查看Service Worker缓存的资源,包括缓存名称、缓存的内容。可以查看、删除缓存。
- IndexedDB:查看IndexedDB中的数据。
- Local Storage / Session Storage:查看本地存储的数据。
Lighthouse审计:
Lighthouse,是Chrome内置的一个自动化审计工具,可以审计你的网站,包括性能、可访问性、最佳实践、SEO,以及PWA。
在开发者工具中,切换到Lighthouse面板,选择要审计的类别(记得勾选Progressive Web App),然后点击Generate report,就可以生成一份详细的审计报告。
报告中,会告诉你你的PWA是否符合标准,有哪些问题需要改进,以及怎么改进。这是检查你的PWA是否合格的最好方式。
八、最佳实践和注意事项
最后,总结一些PWA开发的最佳实践和注意事项:
1. 应用外壳架构(App Shell):
App Shell,是PWA的一种架构模式,就是把应用的外壳(HTML、CSS、JS框架、导航、布局等不经常变化的部分),在Service Worker安装的时候就缓存起来。这样,用户再次访问的时候,外壳可以瞬间从缓存加载,不需要从网络获取,只需要动态加载内容部分。
App Shell架构,能大大提升应用的加载速度,特别是在弱网环境下,用户能很快看到页面的框架,而不是白屏。
2. 缓存版本管理:
Service Worker的缓存,一定要做好版本管理。每次更新静态资源的时候,都要更新缓存名称(比如从my-pwa-v1改成my-pwa-v2),这样,新的Service Worker激活的时候,会删除旧的缓存,用户就能看到最新的内容了。
如果不更新缓存名称,用户可能一直看到旧的缓存内容,更新不生效。
3. 谨慎处理缓存的更新:
使用缓存优先策略的时候,要注意缓存的更新。如果静态资源更新了,但是缓存名称没变,用户可能一直看到旧的内容。
一种常见的做法是,给静态资源的文件名加上哈希值(比如main.abc123.js),这样,文件内容变了,文件名也会变,缓存就会自动更新。
4. 不要缓存不应该缓存的内容:
不是所有的内容都适合缓存。比如,支付接口、用户的个人信息、实时数据等,不应该缓存,否则可能会导致数据不一致或者安全问题。
在Service Worker的fetch事件中,要根据请求的URL和类型,判断哪些可以缓存,哪些不能缓存。
5. 注意Service Worker的更新机制:
Service Worker的更新,有自己的机制。当你更新了sw.js文件,浏览器检测到文件变化(哪怕只有一个字节的变化),就会重新安装新的Service Worker。但是,新的Service Worker安装完成后,会进入等待状态,直到所有的页面都关闭,旧的Service Worker被卸载,新的才会激活。
这意味着,用户可能需要关闭所有的标签页,重新打开,才能看到更新。为了让用户更快地更新,可以在新的Service Worker安装完成后,调用self.skipWaiting(),跳过等待,立即激活。同时,在页面中,监听controllerchange事件,自动刷新页面。
6. 做好降级处理:
不是所有的浏览器都支持PWA的所有特性。在开发的时候,要做好降级处理,确保在不支持Service Worker、不支持推送通知的浏览器上,你的网站也能正常使用,只是没有PWA的特性而已。
这就是"渐进式"的含义:核心功能在所有浏览器上都能用,支持PWA特性的浏览器,能获得更好的体验。
7. 注意iOS的兼容性:
iOS 11.3及以上的Safari,开始支持PWA的核心特性(manifest、Service Worker、添加到主屏幕)。但是,iOS上的PWA,还有一些局限性,比如不支持推送通知、存储空间有限、启动画面的支持不够完善等。
在开发PWA的时候,要注意iOS的兼容性,做好测试和适配。
九、写在最后
PWA,是Web开发的未来趋势之一。它让Web应用拥有了接近原生应用的体验,同时保留了Web应用的优势:免安装、跨平台、易传播、自动更新。
2018年,PWA已经足够成熟,可以在生产环境中使用了。Chrome、Firefox、Safari等主流浏览器,都已经支持PWA的核心特性。越来越多的公司,开始把自己的Web应用改造成PWA,并且获得了很好的效果。
当然,PWA也不是银弹,不是所有的应用都适合做PWA。如果你的应用需要大量的硬件访问(比如摄像头、蓝牙、NFC),或者需要高性能的图形渲染(比如3D游戏),那原生应用可能更合适。但是,对于大部分的内容型、工具型、电商型应用,PWA都是一个很好的选择。
PWA的开发,也不是很难。只要配置好manifest.json,写好Service Worker,做好缓存策略,你的网站就可以变成一个PWA了。当然,要做好一个体验优秀的PWA,还需要在性能、缓存、推送、安装等方面,做更多的优化。
最后,用一句话来结束这篇文章:"PWA,不是一个单一的技术,而是一系列Web技术的集合,它让Web应用拥有了接近原生应用的体验。渐进式、可靠、快速、沉浸式,是PWA的核心特征。如果你还没有尝试过PWA,现在是时候开始了。"
希望这篇文章,能帮助你入门和掌握PWA。如果你有不同的观点或者补充,欢迎在评论区留言,我们一起交流。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录