PWA 在过去的一年可以说十分火爆,各种前端技术会议都有相关课题,我最早知道 PWA 也是在技术会议上。目前 Google 系的浏览器基本已经支持,Apple 系的浏览器也已经在开发者版本中支持(浏览器支持情况[1])。作为一个前端开发者,是时候掌握这一门技能了。
PWA 主要有 Service Worker、Web App Manifest、Web Push 等特性。这次我们先实现 Service Worker 和 Web App Manifest,GoogleChrome 团队提供了一个 Workbox ,对缓存控制进行封装,是十分有用的几个工具。
Web App Manifest
Web App Manifest 的实现十分简单,我们可以借助 App Manifest Generator[2] 快速生成,放置对应资源到合适的目录,然后在页面 head 中增加<link rel="manifest" href="/manifest.json">
即可。
Service Worker
Service Worker 可以帮你预加载资源,拦截或修改网络请求。我们这次主要实现静态资源预加载,网络请求缓存,图片自动支持 webp。前端代码构建完成以后,我们可以自定义一些规则,整理出需要预加载的资源,进行缓存。我们使用 Workbox 命令行工具,扫描出需要预加载的目录,生成 Service Worker 文件。
Workbox 配置文件
主要定义静态文件存放的目录,处理 CDN 地址规则。
// 需要先安装orkbox 命令行工具 npm i workbox-cli -g
// workbox-cli-config.js
const fs = require('fs')
// 业务相关,每个项目需要自己修改
const buildStats = require('./build/build-stats.json')
const buildID = fs.readFileSync('./build/BUILD_ID').toString()
const cdnServer = 'https://o88aw8tfp.qnssl.com'
module.exports = {
globDirectory: './build/', // 静态资源存放目录
globPatterns: [
'app.js', // 自定义扫描规则,与业务相关
'bundles/pages/*.js',
'chunks/*.js'
],
swSrc: 'static/workbox.js', // Service Worker 模板文件,相关业务文件写在这里
swDest: 'static/sw.js', // 最终生成的 Service Worker 文件,不要手动修改
modifyUrlPrefix: { // 静态资源 CDN 路径处理,与业务相关
'app.js': `${cdnServer}/_next/${buildStats['app.js'].hash}/app.js`,
'bundles/pages/': `${cdnServer}/_next/${buildID}/page/`,
'chunks/': `${cdnServer}/_next/${buildID}/webpack/chunks/`
}
}
Workbox 模板文件
实现图片自动转换为 webp 格式,缓存网络请求,以备离线时使用。
// static/workbox.js
/* eslint-env worker */
/* global FetchEvent WorkboxSW */
var workboxCDN = 'https://cdn.jsdelivr.net/npm/[email protected]/build/importScripts/workbox-sw.prod.v2.1.2.min.js'
importScripts(workboxCDN) // 加载 Workbox 库
;(function () {
var workbox = new WorkboxSW({
clientsClaim: true,
skipWaiting: true
}) // 生成 Workbox 实例
var networkFirstStrategy = workbox.strategies.networkFirst({
cacheExpiration: {
maxEntries: 3000,
maxAgeSeconds: 3600 * 24 * 7
}
}) // 创建一个网络请求优先的缓存仓库
var cacheFirstStrategy = workbox.strategies.cacheFirst({
cacheExpiration: {
maxEntries: 3000,
maxAgeSeconds: 3600 * 24 * 7
}
}) // 创建一个缓存优先的缓存仓库
/**
* webp 处理
* 匹配非 webp 的 cdn 图片资源,缓存其 webp 格式
*/
workbox.router.registerRoute(function (args) {
var useWebp = false
var supportWebp = /image\/webp/i.test(args.event.request.headers.get('Accept')) // 通过 HTTP Accept 头信息判断浏览器是否支持 webp
var url = args.url.href.split('?')[0]
if (!supportWebp) {
return useWebp
}
// 判断哪些图片需要转为 webp 格式,业务相关
if (/qnssl\.com/i.test(url) && /(jpg|png)$/i.test(url)) {
useWebp = true
}
return useWebp // 觉得是否需要拦截路由,true 拦截,false 不拦截
}, function (args) {
// 拦截到的请求处理规则
var url = args.url.href.split('?')[0]
// 重新构造 fetch 请求
args.event = new FetchEvent(args.event.type, {
request: new Request(url + '?imageView2/2/q/85/format/webp'), // CDN 转换图片格式参数,各 CDN 不同
clientId: args.event.clientId,
isReload: args.event.isReload
})
return cacheFirstStrategy.handle(args) // 将请求交给缓存仓库处理,如无缓存,会从网络加载
})
workbox.router.registerRoute(/\/.*/i, networkFirstStrategy) // 所有同域下的请求进行缓存,以备离线使用,此处实现比较粗糙,具体项目需要细致化到各类路由
workbox.precache([workboxCDN]) // 缓存 Workbox 库文件
workbox.precache([]) // 占位符,Workbox 命令行工具会转换为具体的静态资源
}())
生成 Service Worker 文件
在你的项目构建流程中增加 workbox inject:manifest
代码, 或者命令行手动执行 workbox inject:manifest
。构建后就可以查看生成的文件。
// 生成的文件存放在 static/sw.js
/* eslint-env worker */
/* global FetchEvent WorkboxSW */
var workboxCDN = 'https://cdn.jsdelivr.net/npm/[email protected]/build/importScripts/workbox-sw.prod.v2.1.2.min.js'
importScripts(workboxCDN)
;(function () {
var workbox = new WorkboxSW({
clientsClaim: true,
skipWaiting: true
})
var networkFirstStrategy = workbox.strategies.networkFirst({
cacheExpiration: {
maxEntries: 3000,
maxAgeSeconds: 3600 * 24 * 7
}
})
var cacheFirstStrategy = workbox.strategies.cacheFirst({
cacheExpiration: {
maxEntries: 3000,
maxAgeSeconds: 3600 * 24 * 7
}
})
/**
* webp 处理
* 匹配非 webp 的 cdn 图片资源,缓存其 webp 格式
*/
workbox.router.registerRoute(function (args) {
var useWebp = false
var supportWebp = /image\/webp/i.test(args.event.request.headers.get('Accept'))
var url = args.url.href.split('?')[0]
if (!supportWebp) {
return useWebp
}
if (/qnssl\.com/i.test(url) && /(jpg|png)$/i.test(url)) {
useWebp = true
}
return useWebp
}, function (args) {
var url = args.url.href.split('?')[0]
// 重新构造 fetch 请求
args.event = new FetchEvent(args.event.type, {
request: new Request(url + '?imageView2/2/q/85/format/webp'),
clientId: args.event.clientId,
isReload: args.event.isReload
})
return cacheFirstStrategy.handle(args)
})
workbox.router.registerRoute(/\/.*/i, networkFirstStrategy)
workbox.precache([workboxCDN])
workbox.precache([
{
"url": "https://o88aw8tfp.qnssl.com/_next/fc0a873a0fb8865c733eafd4361ee446/app.js",
"revision": "fc0a873a0fb8865c733eafd4361ee446"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/_document.js",
"revision": "35b6335d59f61b737f1b156c134cf2c2"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/_error.js",
"revision": "ebb56565bb86beef4465afad58568033"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/list.js",
"revision": "b48b7fbc258e95243ecacd6ba28c5415"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/post.js",
"revision": "2319d8d3db08749e2b2aa42cdad347e7"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/page/tag.js",
"revision": "58e377967c2f6212faffe819b2d642be"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/baguettebox_7753f32ed8b1b354d9d9a9f46540e36c.js",
"revision": "25b303719d46f152f12649f043dda480"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/components_Disqus_330b0a098ae588ad4dde7ddac1342b19.js",
"revision": "a23c97a37071b2128700eb97b569ff70"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/nprogress_cc31a9d7a86a6f32932208d2b28f4f1c.js",
"revision": "bb5f8920b823835f1828dc98a17ffd82"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/utils_inject_57190832e9ae540e620d616b9e53c208.js",
"revision": "f78bb021995310ff2cd09892e6d496aa"
},
{
"url": "https://o88aw8tfp.qnssl.com/_next/3b1a9146-b72d-4133-80dc-bff13eff5969/webpack/chunks/utils_lozad_e11a80f493f09965eee540dea545759d.js",
"revision": "2cab0d134cb64502de91469894863741"
}
])
}())
可以看到,生成的 Service Worker 文件已经包含需要预加载的文件。
注册 Service Worker
function register () {
if (navigator && navigator.serviceWorker && navigator.serviceWorker.register) {
return navigator.serviceWorker.register(`/sw.js`, { // 注册 Service Worker,与作用域相关,不能乱放
scope: '/'
})
} else {
return Promise.resolve()
}
}
function unregister () {
if (navigator && navigator.serviceWorker && navigator.serviceWorker.getRegistrations) {
return navigator.serviceWorker.getRegistrations().then(registrations => {
for (let registration of registrations) {
registration.unregister()
}
})
} else {
return Promise.resolve()
}
}
window.addEventListener('load', function () {
if ('serviceWorker' in navigator) {
register().catch(unregister)
}
})
测试
经过前面的流程,基本开发工作已经完成,现在可以在浏览器进行测试了,上线前一定记得测浏览器兼容。
几张成功的截图
分享
PWA 开发完成上线后,你可以将网站提交到 PWA Directory, 与更多的人分享。
如果你想查看我的网站记录,你可以访问 https://pwa-directory.appspot.com/pwas/5070467353280512。
Congratulations, you were selected for a random upvote! Follow @resteemy and upvote this post to increase your chance of being upvoted again!
Read more about @resteemy here.
Congratulations @ccbikai! You received a personal award!
Click here to view your Board
Congratulations @ccbikai! You received a personal award!
You can view your badges on your Steem Board and compare to others on the Steem Ranking
Vote for @Steemitboard as a witness to get one more award and increased upvotes!