作者:Jiang, Jilin
引言:当网站无法访问时,很少有用户会给你发一封邮件或者电话告诉他遇到的状况。利用最新的PWA技术,你可以直接自行统计。更棒的是,网站基本无需改动。
在文章开始前,如果你对PWA还没有任何了解。建议访问google developer阅读相关内容:https://developers.google.com/web/progressive-web-apps/。当然,下文也将简单的行进介绍。
PWA全称为ProgressiveWeb Apps(渐进式网页应用),使得网页可以像原生App一样运作。在离线监控中,我们目前只需要用到ServiceWorker的离线功能。
当用户初次访问网站的时候,我们通过注册ServiceWorker来对网站请求进行监控:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Offline</title>
</head>
<body>
<h1>Hello World</h1>
<script>
if (typeof navigator !== 'undefined' && navigator.serviceWorker) {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW register success:', registration);
}).catch((error) => {
console.log('SW register failed:', error);
});
}
</script>
</body>
</html>
navigator.serviceWorker.register用于对service worker进行注册。由于register是一个异步操作,所以用户第一次访问到service worker加载完成前,serviceworker并不会生效。当service worker注册完成后,我们便可以对用户访问进行监控了。
备注:serviceworker默认对根目录进行注册,如果你的网站存于二级目录之下需要手工指定scope:
navigator.serviceWorker.register('/offline/sw.js', { scope: '/offline/' })
在sw.js中,我们对service worker进行install和active:
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim());
});
其中,install事件用于监听service worker注册。当注册完毕后,在waitUtil中通过添加cache.add来缓存你需要的文件。此处由于我们不需要缓存,直接使用skipWaiting来跳过等待。
而claim的作用是激活未受控制的客户端。调用该方法后,会接管所有在相同scope下的页面。配合skipWaiting可以使调用该方法的service worker立刻接管所有active的client。
接着,我们对fetch事件进行注册。
self.addEventListener('fetch', (event) => {
const request = event.request;
console.log('Current request:', request);
});
接着,我们对fetch请求进行接管。之后就可以在network里查看到请求都将经由Serviceworker代理:
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(request)
);
});
接着,我们对fetch进行改造。添加对错误处理的捕获操作:
const promise = fetch(request);
event.respondWith(promise);
promise.then(res => {
if(res.ok) return;
// Status catch
}).catch((err) => {
// Error catch
});
这里需要注意的是,fetch方法只会reject网络错误或者其他任何阻止请求完成的操作而不会rejecthttp error status,所以诸如404或者500错误不会被捕获。你需要对response的ok属性进行判断。如果为false则说明这次请求服务器返回错误代码。
以上,我们就可以很方便的捕获全局的fetch错误并将其发送给服务器用于定位:
// Error Log
const report = {
url: request.url,
method: request.method,
mode: request.mode,
referrer: request.referrer,
};
promise.then(res => {
if(res.ok) return;
// Status catch
report.status = res.status;
report.message = res.statusText;
return report;
}).catch((err) => {
// Error catch
report.message = err.message;
return report;
}).then(report => {
if (!report) return;
fetch('/report', {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(report),
});
});
在服务端,监听错误信息:
Error Report: { url: 'http://localhost:2333/test1.json',
method: 'GET',
mode: 'cors',
referrer: 'http://localhost:2333/index',
status: 404,
message: 'Not Found' }
Error Report: { url: 'http://localhost:2333/test1.json',
method: 'GET',
mode: 'cors',
referrer: 'http://localhost:2333/index',
status: 404,
message: 'Not Found' }
完成错误回传后,我们已经可以对网站的异步错误信息进行全局监控。但是这并不是PWA最强健的地方。如果我们当前的网站无法访问,通过post请求回传错误报告都将会失败。对此,我们需要独立于当前网站增设另外的错误处理站点用于接收错误统计信息:
const cors = require('cors');
const app = new (Express)();
app.options('/report', cors());
app.post('/report', cors(), function (req, res) {
console.log('Error Report:', req.body);
res.end();
});
app.listen(5566, function () {
console.log('listen 5566...');
});
其中cors组件用于增加跨域支持,fetch方法会预发送options请求以查询是否可以进行跨域访问。所以我们除了post开启cors外,options需要同样开启。
接着,回到我们之前的service worker。添加离线web的缓存:self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('cache').then(
cache => cache.add('/offline.html').then(self.skipWaiting())
)
);
});
然后,对首页进行额外的访问性检测处理。如果无法访问,则返回offline页面:
const promise = fetch(request);
if (/\/index$/.test(request.url)) {
event.respondWith(
promise.catch(() => caches.match('/offline.html'))
);
} else {
event.respondWith(promise);
}
完成这些后,你的异步统计便完成了。用户在网站访问失败时,你的统计服务器可以自动记录下用户访问失败的记录。当然,我们仍然有一种情况无法监控到,那就是如果当当前网站无法访问并且错误记录网站同样无法访问(例如用户断网,虽然这种情况我们其实可以不用记录)。那么我们就需要对错误报告进行缓存,当网络恢复正常后再次发送。
这时,很容易想到使用localStorage进行存储。但是如果你这么做了,你会发现以下错误信息:
没错,localStorage在service worker中是无法访问的。但是你可以使用IndexedDB来进行存储,这里我们对report新增一个timestamp属性作为key:
let db;
const DBRquest = indexedDB.open('sw');
DBRquest.onerror = function(event) {
console.log('Database error: ' + event.target.errorCode);
};
DBRquest.onsuccess = function(event) {
db = event.target.result;
console.log('Database open success:', db);
};
DBRquest.onupgradeneeded = function(event) {
db = event.target.result;
const objectStore = db.createObjectStore('reports', { keyPath: 'timestamp' });
};
function saveReport(report) {
if (!db) return;
const transaction = db.transaction('reports', 'readwrite');
const store = transaction.objectStore('reports');
store.add(report);
}
然后对post report进行改造。如果无法发送,则临时存储于本地(之后代码省略indexedDB与serviceworker的异步处理):
fetch('http://localhost:5566/report', {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(report),
}).catch(() => {
console.log('Report post error');
saveReport(report);
});
在IndexedDB便可以查询到:
接着,在active事件使用游标来进行遍历:
const transaction = db.transaction('reports', 'readwrite');
const store = transaction.objectStore('reports');
store.openCursor().onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.key, '->', cursor.value);
cursor.continue();
}
};
最后,在post完成后不要忘记清理IndexDB便可。这样,便完成了完整的异步监控。即便记录网站临时不可用,你仍然不用担心数据丢失。在网站恢复后,仍可以将统计信息进行补足。