一、Pointer Events
简介:
指针事件 - Pointer events 是一类可以为定点设备所触发的DOM事件。它们被用来创建一个可以有效掌握各类输入设备(鼠标、触控笔和单点或多点的手指触摸)的统一的DOM事件模型。所谓指针是指一个可以明确指向屏幕上某一组坐标的硬件设备。建立这样一个单独的事件模型可以有效的简化Web站点与应用所需的工作,同时也便于提供更加一致与良好的用户体验,无需关心不同用户和场景在输入硬件上的差异。而且,对于某些特定设备才可处理的交互情景,指针事件也定义了一个pointerType 属性以使开发者可以知晓该事件的触发设备,以有针对性的加以处理。属性navigator.maxTouchPoints 被设计用来指明在同一时间点所支持的最大的触摸点数量。
active pointer(活跃指针):任意指针输入设备都可以产生事件。一个可以产生后继事件的指针可以被认为是一个活跃指针。例如,一个触摸笔处于压下状态时可以认为是活跃的,因为它接下来的抬起或移动都会产生额外的后继事件。
digitizer(数位设备):一个可以检测其表面接触行为的传感设备。通常来说,其所用的传感设备是一个可以感知由某些输入设备(如触控笔、压感笔、手指等)所提供的输入信息的可触摸屏幕。
hit test(命中检测):浏览器用以检测某一指针事件的目标元素的过程。通常来说,这一过程是通过比照出现在文档或屏幕媒介上的指针位置与视觉布局来实现的。
pointer(指针 ):某个呈现形式并不确定的硬件,该硬件可以指向一个(或一组)屏幕上特定坐标。典型的指针输入设备有鼠标、触控笔、手指触控点等。
pointer capture(指针捕捉):指针捕捉能够允许某些事件的产生。这些事件在指针将要重新指向一些并非通过命中检测而给定元素时触发。
pointer event(指针事件 ):dom触发的指针事件。
首选指针:
随着设备的发展,设备可能存在多个指针(比如某设备同时拥有触摸屏和鼠标)或者一个指针设备支持多个接触点(例如支持多点触控的触摸屏)。应用开发时,可以使用isPrimary属性来识别每类指针的一组指针输入中的主要指针。如果应用仅希望对首选指针提供支持,则可以忽略其他的指针事件。对于鼠标来说,只有一个指针输入,所以这一输入将一直是首选指针。对于触摸输入来说,当用户在触摸屏幕,且没有其他活跃指针时,会被认做首选指针。对于压感笔输入来说,当用户的笔触开始接触屏幕或平面,且当时没有其他的活跃笔触在接触屏幕时,该输入将被认作首选指针。
touch-action:
CSS属性touch-action被用来指明浏览器是否应当对某一区域的触摸事件应用其默认行为(例如放大或旋转等)。
属性取值为:
auto:当触控事件发生在元素上时,由浏览器来决定进行哪些操作,比如对viewport进行平滑、缩放等。
none:当触控事件发生在元素上时,不进行任何操作。
pan-x:启用单指水平平移手势。可以与 pan-y 、pan-up、pan-down 和/或 pinch-zoom 组合使用。
pan-y:启用单指垂直平移手势。可以与 pan-x 、pan-left 、pan-right 和/或 pinch-zoom 组合使用。
manipulation:浏览器只允许进行滚动和持续缩放操作。任何其它被auto值支持的行为不被支持。启用平移和缩小缩放手势,但禁用其他非标准手势,例如双击以进行缩放。 禁用双击可缩放功能可减少浏览器在用户点击屏幕时延迟生成点击事件的需要。 这是“pan-x pan-y pinch-zoom”(为了兼容性本身仍然有效)的别名。
pan-left,pan-right,pan-down,pan-up:启用以指定方向滚动开始的单指手势。 一旦滚动开始,方向可能仍然相反。 请注意,滚动“向上”(pan-up)意味着用户正在将其手指向下拖动到屏幕表面上,同样 pan-left 表示用户将其手指向右拖动。 多个方向可以组合,除非有更简单的表示(例如,“pan-left pan-right”无效,因为“pan-x”更简单,而“pan-left pan-down”有效)。
pinch-zoom:启用多手指平移和缩放页面。 这可以与任何平移值组合。
/**
* touch-action示例
*/
#map {
touch-action: none;
}
按钮状态:
对于某些指针设备来说,比如鼠标或者压感笔,设备上可能有一个或多个按钮可以同时或依次序按动。比如在某个按钮释放后立刻按下其他按钮。为了确定这些按钮的按压状态,指针事件使用button与 buttons等MouseEvent接口中的事件 (PointerEvent
继承于此)表明相应的状态。
按钮状态 | button | buttons(哪些鼠标按键被按下,复合操作,多操作“|”分割) |
鼠标移动且无按钮被按压 | -1 | 0 |
鼠标左键、触摸接触、压感笔接触(无额外按钮被按压) | 0 | 1 |
鼠标中键 | 1 | 4 |
鼠标右键、压感笔接触且笔杆按钮被按压 | 2 | 2 |
鼠标X1 (back) | 3 | 8 |
鼠标X2 (forward) | 4 | 16 |
压感笔接触且橡皮擦按钮被按压 | 5 | 32 |
pointer事件:
事件 | 监听事件 | 描述 |
pointerover | onpointerover | 当定点设备进入某个元素的命中检测范围时触发。 |
pointerenter | onpointerenter | 当定点设备进入某个元素或其子元素的命中检测范围时,或做为某一类不支悬停(hover)状态的设备所触发的poinerdown事件的后续事件时所触发。(详情可见 pointerdown事件类型). |
pointerdown | onpointerdown | 当某指针得以激活时触发(如果是鼠标行为则鼠标按下触发)。 |
pointermove | onpointermove | 当某指针改变其坐标时触发。 |
pointerup | onpointerup | 当某指针不再活跃时触发。 |
pointercancel | onpointercancel | 当浏览器认为某指针不会再生成新的后续事件时触发(例如某设备不再活跃) |
pointerout | onpointerout | 可能由若干原因触发该事件,包括:定位设备移出了某命中检测的边界;不支持悬浮状态的设备发生pointerup事件(见pointerup事件); 作为 pointercancel event事件的后续事件(见pointercancel事件);当数位板检测到数位笔离开了悬浮区域时。 |
pointerleave | onpointerleave | 当定点设备移出某元素的命中检测边界时触发。对于笔形设备来说,当数位板检测到笔移出了悬浮范围时触发 |
gotpointercapture | ongotpointercapture | 当某元素接受到一个指针捕捉时触发。 |
lostpointercapture | onlostpointercapture | 当对某个指针的指针捕捉得到释放时触发。 |
pointer event事件属性:
Event属性 | 描述 |
pointerId | 对于某个由指针引起的事件的唯一标识。 |
width | 以CSS像素计数的宽度属性,取决于指针的接触面的几何构成 |
height | 以CSS像素计数的高度属性,取决于指针的接触面的几何构成 |
pressure | 规范化后的指针输入的压力值,取值范围为0到1,0代表硬件可检测到的压力最小值,1代表最大值 |
tiltX | 在Y- z平面与同时包含换能器(如笔)轴和Y轴的平面之间的平面角(以角度为单位,在-90到90之间)。 |
tiltY | 在X- z平面与同时包含换能器(如笔)轴和X轴的平面之间的平面角(以角度为单位,在-90到90之间) |
pointerType | 表明引发该事件的设备类型(鼠标/笔/触摸等) |
isPrimary | 表示该指针是否为该类型指针中的首选指针 |
示例:
<html>
<head>
<title></title>
</head>
<body">
<div id="target"> Touch me ... </div>
<script>
var id = -1;
function process_id(event) {
// Process this event based on the event's identifier
}
function process_mouse(event) {
// Process the mouse pointer event
}
function process_pen(event) {
// Process the pen pointer event
}
function process_touch(event) {
// Process the touch pointer event
}
function process_tilt(tiltX, tiltY) {
// Tilt data handler
}
function process_pressure(pressure) {
// Pressure handler
}
function process_non_primary(event) {
// Pressure handler
}
function cancel_handler(event){
var el=document.getElementById("target");
// Release the pointer capture
el.releasePointerCapture(ev.pointerId);
}
function down_handler(ev) {
// Calculate the touch point's contact area
var area = ev.width * ev.height;
//Element 'target' will receive/capture further events
document.getElementById("target").setPointerCapture(ev.pointerId);
// Compare cached id with this event's id and process accordingly
if (id == ev.identifier) process_id(ev);
// Call the appropriate pointer type handler
switch (ev.pointerType) {
case "mouse":
process_mouse(ev);
break;
case "pen":
process_pen(ev);
break;
case "touch":
process_touch(ev);
break;
default:
console.log("pointerType " + ev.pointerType + " is Not suported");
}
// Call the tilt handler
if (ev.tiltX != 0 && ev.tiltY != 0) process_tilt(ev.tiltX, ev.tiltY);
// Call the pressure handler
process_pressure(ev.pressure);
// If this event is not primary, call the non primary handler
if (!ev.isPrimary) process_non_primary(evt);
}
window.onload = function (){
var el=document.getElementById("target");
// Register pointerdown handler
el.onpointerdown = down_handler;
el.onpointercancel = cancel_handler;
}
</script>
</body>
</html>
二、Service Worker
简介:
为了使移动应用程序为用户所用(任何设备上都可以使用),2015年谷歌提出了渐进式web应用程序(PWA)。而Service Worker就是PWA中最重要的一环。Service Worker从Web Worker的基础上发展而来,可以为 JavaScript 创建运行在后台的额外线程,并被多个页面共享(但不能访问dom)。IE和safari未实现该api。
Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。处于安全考虑,Service Worker只能在localhost域以及HTTPS域下访问。
流程:
用户首次访问service worker控制的网站或页面时,service worker会立刻被下载。之后至少每24小时它会被下载一次。它可能被频繁地下载,同时每24小时一定会被下载一次,以避免不良脚本长时间生效。一旦它与现有service worker不同(字节对比),如果下载的文件是新的,安装就会尝试进行。如果现有service worker已启用,新版本会在后台安装,但不会被激活,这个时序称为worker in waiting。直到所有已加载的页面不再使用旧的service worker才会激活新的service worker。只要页面不再依赖旧的service worker,新的service worker会被激活(成为active worker)。
生命周期:
register:注册过程独立于网页,先在页面执行注册,之后在浏览器后台启动安装步骤。
install:通常需要缓存某些静态资源。当所有文件已成功缓存,则安装完毕。如果任何文件下载失败或缓存失败,则安装失败,无法激活。
activate:管理就缓存的绝佳机会。激活后它将会对作用域页面实时控制,不过首次注册该服务工作线程的页面需要再次加载才会受其控制。
fetch:处于两种状态之一,终止以节省内存或者监听获取 fetch 和消息 message 事件
destory:由浏览器决定,因此尽量不要留存全局变量。
示例:
/**
* service worker
*/
var cacheName = '0.0.1'; //缓存静态资源版本
var apiCacheName = '0.0.1'; //api缓存
var cacheFiles = [
'./pushApi.html',
'./images/icon.jpg',
'./images/webworker.jpg',
'./images/performance.png',
'./js/pushApi.js'
];
// 监听install事件,安装完成后,进行文件缓存
self.addEventListener('install', function(e) {
console.log('Service Worker 状态: install');
var cacheOpenPromise = caches.open(cacheName).then(function(cache) {
return cache.addAll(cacheFiles);
});
e.waitUntil(cacheOpenPromise);
});
// 监听activate事件,激活后通过cache的key来判断是否更新cache中的静态资源
self.addEventListener('activate', function(e) {
console.log('Service Worker 状态: activate');
var cachePromise = caches.keys().then(function(keys) {
return Promise.all(
keys.map(function(key) {
if (key !== cacheName && key !== apiCacheName) {
return caches.delete(key);
}
})
);
});
e.waitUntil(cachePromise);
// 注意不能忽略这行代码,否则第一次加载会导致fetch事件不触发
return self.clients.claim();
});
self.addEventListener('fetch', function(e) {
// 需要缓存的xhr请求
var cacheRequestUrls = ['/book?'];
console.log('现在正在请求:' + e.request.url);
// 判断当前请求是否需要缓存
var needCache = cacheRequestUrls.some(function(url) {
return e.request.url.indexOf(url) > -1;
});
if (needCache) {
// 需要缓存
// 使用fetch请求数据,并将请求结果clone一份缓存到cache
// 此部分缓存后在browser中使用全局变量caches获取
caches.open(apiCacheName).then(function(cache) {
return fetch(e.request).then(function(response) {
cache.put(e.request.url, response.clone());
return response;
});
});
} else {
// 非api请求,直接查询cache
// 如果有cache则直接返回,否则通过fetch请求
e.respondWith(
caches
.match(e.request)
.then(function(cache) {
return cache || fetch(e.request);
})
.catch(function(err) {
console.log(err);
return fetch(e.request);
})
);
}
});
// 添加service worker对push的监听
self.addEventListener('push', function(e) {
var data = e.data;
if (data) {
data = data.json();
console.log('push的数据为:', data);
self.registration.showNotification(data.text);
} else {
console.log('push没有任何数据');
}
});
//注册function registerServiceWorker(file) {
return navigator.serviceWorker.register(file);
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function subscribeUserToPush(registration, publicKey) {
var subscribeOptions = {
userVisibleOnly: true, //表明该推送是否需要显性地展示给用户,即推送时是否会有消息提醒。如果没有消息提醒就表明是进行“静默”推送在Chrome中,必须要将其设置为true,否则浏览器就会在控制台报错
applicationServerKey: urlBase64ToUint8Array(publicKey)
};
return registration.pushManager
.subscribe(subscribeOptions)
.then(function(pushSubscription) {
console.log('Received PushSubscription: ', pushSubscription);
return pushSubscription;
})
.catch(function(err, val1) {
console.log('err111:', err);
});
}
function sendSubscriptionToServer(body, url) {
url = url || '/subscription';
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.timeout = 60000;
xhr.onreadystatechange = function() {
var response = {};
if (xhr.readyState === 4 && xhr.status === 200) {
try {
response = JSON.parse(xhr.responseText);
} catch (e) {
response = xhr.responseText;
}
resolve(response);
} else if (xhr.readyState === 4) {
resolve();
}
};
xhr.onabort = reject;
xhr.onerror = reject;
xhr.ontimeout = reject;
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(body);
});
}
window.onload = function() {
if ('serviceWorker' in navigator && 'PushManager' in window) {
var publicKey = 'BOEQSjdhorIf8M0XFNlwohK3sTzO9iJwvbYU-fuXRF0tvRpPPMGO6d_gJC_pUQwBT7wD8rKutpNTFHOHN3VqJ0A'; //客户端的公钥
// 注册service worker
registerServiceWorker('./serviceWork.js') //此处需要注意,这个serviceWork.js只能放到根目录中
.then(function(registration) {
console.log('Service Worker 注册成功');
// 开启该客户端的消息推送订阅功能
return subscribeUserToPush(registration, publicKey);
})
.then(function(subscription) {
var body = { subscription: subscription };
// 为了方便之后的推送,为每个客户端简单生成一个标识
body.uniqueid = new Date().getTime();
// 将生成的客户端订阅信息存储在自己的服务器上
return sendSubscriptionToServer(JSON.stringify(body));
})
.then(function(res) {
console.log(res);
})
.catch(function(err) {
console.log(err);
});
}
};
三、Push Api
简介:
Push API 给与了Web应用程序接收从服务器发出的推送消息的能力,无论Web应用程序是否在用户设备前台,甚至刚加载完成。这样,开发人员就可以向用户投放异步通知和更新,从而让用户能更及时地获取新内容。通常情况下Push API(消息推送)与Notifications API(消息提醒)一起使用。
push API的实现通过浏览器实现的push service(专门的Push服务,你可以认为是一个第三方服务,目前chrome与firefox都有自己的Push Service Service。理论上只要浏览器支持,可以使用任意的Push Service)进行服务的请求以及处理,它通过客户端与push service进行连接,获取相关内容,并传递到后端服务器,之后push service将会维护客户端与后端服务连接,并最终通过服务端发起消息推送。总而言之,push service担任了中间服务器的作用。
Push Service:可以接收网络请求,核验请求后并将数据最终推送给对应的客户端。当用户离线时,可以帮我们保存消息队列,直到用户联网后再发送给他们。Push Service会为每个发起订阅的浏览器生成一个唯一的URL,这样,我们在服务端推送消息时,向这个URL进行推送后,Push Service就会知道要通知哪个浏览器。这个url能够通过service worker订阅后获得。
下图展示完整的连接过程(订阅和推送):
订阅阶段:
订阅将会由客户端发起,并经历Ask Permission->Subscribe->Monitor->Distribute Push Resource过程。
Ask Permission:浏览器会询问是否允许用户通知,获取用户授权后才允许后续的所有操作。该提示为浏览器策略,以防开发者非法获取用户的隐私及数据等,因此需要用户授权
Subscribe:客户端需要向push service发起订阅,push service类似于中间层代理服务器,订阅后会得到pushSubscription对象,后续所有操作将会依赖该对象进行发送及处理。
Monitor:订阅操作会和Push Service进行通信,生成相应的订阅信息,Push Service会维护相应信息,并基于此保持与客户端的联系。
Distribute Push Resource:为了更好的支持服务器端推送服务,因此生成的订阅信息需要保存到服务器端,并通过服务器端发起消息推送请求。
推送阶段:
首先,当服务器需要推送消息时,服务器不直接与客户端建立连接,而是由服务器与push Service通过web push协议建立连接,将推送内容传递给push service。
其次,push service接收到数据后,会根据维护的客户端信息,校验部分用户信息,将消息推送给订阅的客户端
最后,客户端浏览器接收到推送的消息后,完成消息推送。
Push Service安全机制:在web push中,为了保证客户端只会收到其订阅的服务端推送的消息,需要对推送信息进行数字签名。在Web Push中会有一对公钥与私钥。客户端持有公钥,而服务端持有私钥。客户端在订阅时,会将公钥发送给Push Service,而Push Service会将该公钥与相应的url维护起来。而当服务端要推送消息时,会使用私钥对发送的数据进行数字签名,并根据数字签名生成Authorization
请求头。Push Service收到请求后,根据url取到公钥,对数字签名解密验证,如果信息相符则表明该请求是通过对应的私钥加密而成,也表明该请求来自浏览器所订阅的服务端。反之亦然。
示例:
//客户端代码
function registerServiceWorker(file) {
return navigator.serviceWorker.register(file);
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function subscribeUserToPush(registration, publicKey) {
var subscribeOptions = {
userVisibleOnly: true, //表明该推送是否需要显性地展示给用户,即推送时是否会有消息提醒。如果没有消息提醒就表明是进行“静默”推送在Chrome中,必须要将其设置为true,否则浏览器就会在控制台报错
applicationServerKey: urlBase64ToUint8Array(publicKey)
};
return registration.pushManager
.subscribe(subscribeOptions)
.then(function(pushSubscription) {
console.log('Received PushSubscription: ', pushSubscription);
return pushSubscription;
})
.catch(function(err, val1) {
console.log('err111:', err);
});
}
function sendSubscriptionToServer(body, url) {
url = url || '/subscription';
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.timeout = 60000;
xhr.onreadystatechange = function() {
var response = {};
if (xhr.readyState === 4 && xhr.status === 200) {
try {
response = JSON.parse(xhr.responseText);
} catch (e) {
response = xhr.responseText;
}
resolve(response);
} else if (xhr.readyState === 4) {
resolve();
}
};
xhr.onabort = reject;
xhr.onerror = reject;
xhr.ontimeout = reject;
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(body);
});
}
window.onload = function() {
if ('serviceWorker' in navigator && 'PushManager' in window) {
var publicKey = 'BOEQSjdhorIf8M0XFNlwohK3sTzO9iJwvbYU-fuXRF0tvRpPPMGO6d_gJC_pUQwBT7wD8rKutpNTFHOHN3VqJ0A'; //客户端的公钥
// 注册service worker
registerServiceWorker('./serviceWork.js') //此处需要注意,这个serviceWork.js只能放到根目录中
.then(function(registration) {
console.log('Service Worker 注册成功');
// 开启该客户端的消息推送订阅功能
return subscribeUserToPush(registration, publicKey);
})
.then(function(subscription) {
var body = { subscription: subscription };
// 为了方便之后的推送,为每个客户端简单生成一个标识
body.uniqueid = new Date().getTime();
// 将生成的客户端订阅信息存储在自己的服务器上
return sendSubscriptionToServer(JSON.stringify(body));
})
.then(function(res) {
console.log(res);
})
.catch(function(err) {
console.log(err);
});
}
};
//serviceWorker.js
var cacheName = '0.0.1'; //缓存静态资源版本
var apiCacheName = '0.0.1'; //api缓存
var cacheFiles = [
'./pushApi.html',
'./images/icon.jpg',
'./images/webworker.jpg',
'./images/performance.png',
'./js/pushApi.js'
];
// 监听install事件,安装完成后,进行文件缓存
self.addEventListener('install', function(e) {
console.log('Service Worker 状态: install');
var cacheOpenPromise = caches.open(cacheName).then(function(cache) {
return cache.addAll(cacheFiles);
});
e.waitUntil(cacheOpenPromise);
});
// 监听activate事件,激活后通过cache的key来判断是否更新cache中的静态资源
self.addEventListener('activate', function(e) {
console.log('Service Worker 状态: activate');
var cachePromise = caches.keys().then(function(keys) {
return Promise.all(
keys.map(function(key) {
if (key !== cacheName && key !== apiCacheName) {
return caches.delete(key);
}
})
);
});
e.waitUntil(cachePromise);
// 注意不能忽略这行代码,否则第一次加载会导致fetch事件不触发
return self.clients.claim();
});
self.addEventListener('fetch', function(e) {
// 需要缓存的xhr请求
var cacheRequestUrls = ['/book?'];
console.log('现在正在请求:' + e.request.url);
// 判断当前请求是否需要缓存
var needCache = cacheRequestUrls.some(function(url) {
return e.request.url.indexOf(url) > -1;
});
if (needCache) {
// 需要缓存
// 使用fetch请求数据,并将请求结果clone一份缓存到cache
// 此部分缓存后在browser中使用全局变量caches获取
caches.open(apiCacheName).then(function(cache) {
return fetch(e.request).then(function(response) {
cache.put(e.request.url, response.clone());
return response;
});
});
} else {
// 非api请求,直接查询cache
// 如果有cache则直接返回,否则通过fetch请求
e.respondWith(
caches
.match(e.request)
.then(function(cache) {
return cache || fetch(e.request);
})
.catch(function(err) {
console.log(err);
return fetch(e.request);
})
);
}
});
/* ============== */
/* push处理相关部分 */
/* ============== */
// 添加service worker对push的监听
self.addEventListener('push', function(e) {
var data = e.data;
if (data) {
data = data.json();
console.log('push的数据为:', data);
self.registration.showNotification(data.text);
} else {
console.log('push没有任何数据');
}
});
//服务端代码
const Koa = require('koa'); // Koa 为一个class
const Router = require('koa-router'); // koa 路由中间件
const app = new Koa();
const router = new Router(); // 实例化路由
const koaStatic = require('koa-static'); //静态资源服务插件
const path = require('path'); //路径管理
const bodyParser = require('koa-bodyparser');
const fs = require('fs');
const store = require('./store');
const webPush = require('web-push'); //消息推送,其实就是使用http与push api生成的endpoint中的url,并且该包主要是封装了请求头的Authorization验证方法
/**
* VAPID值
*/
const vapidKeys = {
publicKey: 'BOEQSjdhorIf8M0XFNlwohK3sTzO9iJwvbYU-fuXRF0tvRpPPMGO6d_gJC_pUQwBT7wD8rKutpNTFHOHN3VqJ0A',
privateKey: 'TVe_nJlciDOn130gFyFYP8UiGxxWd3QdH6C5axXpSgM'
};
webPush.setVapidDetails('mailto:test@qq.com', vapidKeys.publicKey, vapidKeys.privateKey);
app.use(bodyParser());
app.use(koaStatic(path.join(__dirname, './html5')));
router.post('/subscription', async (ctx) => {
let body = ctx.request.body;
store.save(body);
ctx.response.body = {
status: 0
};
});
function pushMessage(subscription, data) {
webPush
.sendNotification(subscription, data)
.then((data) => {
console.log('push service的相应数据:', JSON.stringify(data));
return;
})
.catch((err) => {
// 判断状态码,440和410表示失效
if (err.statusCode === 410 || err.statusCode === 404) {
return store.remove(subscription);
} else {
console.log(subscription);
console.log(err);
}
});
}
router.post('/push', async (ctx) => {
let { uniqueid, payload } = ctx.request.body;
let list = uniqueid ? await store.find({ uniqueid }) : await store.findAll();
let status = list.length > 0 ? 0 : -1;
for (let i = 0; i < list.length; i++) {
let subscription = list[i].subscription;
pushMessage(subscription, JSON.stringify(payload));
}
ctx.response.body = {
status
};
});
app.use(router.routes());
app.listen(3001, () => {
console.log('This server is running at http://localhost:' + 3001);
});
四、Cache
简介:
Cache为新增Web API,主要为request和response对象提供存储机制。Cache 接口像 workers 一样,是暴露在 window 作用域下的。尽管它被定义在 service worker 的标准中, 但是它不一定要配合 service worker 才能使用。一个域可以有多个命名 Cache 对象。需要在脚本中处理缓存更新的方式。缓存更新方式除了明确地更新缓存外,缓存将不会被更新。同时缓存数据不存在过期情况,当数据无效后,需要主动删除。同时需要定期地清理缓存条目,因为每个浏览器都硬性限制了一个域下缓存数据的大小。
API(request指发起的request请求):
Cache.match(request,options):返回一个 Promise对象,resolve的结果是跟 Cache 对象匹配的第一个已经缓存的请求。
Cache.matchAll(request,options):返回一个Promise对象,resolve的结果是跟Cache 对象匹配的所有请求组成的数组。
Cache.add(request):抓取这个URL, 检索并把返回的response对象添加到给定的Cache对象.这在功能上等同于调用 fetch(), 然后使用 Cache.put() 将response添加到cache中.
Cache.addAll(requests):抓取一个URL数组,检索并把返回的response对象添加到给定的Cache对象。
Cache.put(request,response):同时抓取一个请求及其响应,并将其添加到给定的cache。
Cache.delete(request,options):搜索key值为request的Cache 条目。如果找到,则删除该Cache 条目,并且返回一个resolve为true的Promise对象;如果未找到,则返回一个resolve为false的Promise对象。
Cache.keys(request,options):返回一个Promise对象,resolve的结果是Cache 对象key值组成的数组。
示例:
var CACHE_VERSION = 1;
// Shorthand identifier mapped to specific versioned cache.
var CURRENT_CACHES = {
font: 'font-cache-v' + CACHE_VERSION
};
self.addEventListener('activate', function(event) {
var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) {
return CURRENT_CACHES[key];
});
// Active worker won't be treated as activated until promise resolves successfully.
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (expectedCacheNames.indexOf(cacheName) == -1) {
console.log('Deleting out of date cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
self.addEventListener('fetch', function(event) {
console.log('Handling fetch event for', event.request.url);
event.respondWith(
// Opens Cache objects that start with 'font'.
caches.open(CURRENT_CACHES['font']).then(function(cache) {
return cache.match(event.request).then(function(response) {
if (response) {
console.log(' Found response in cache:', response);
return response;
}
}).catch(function(error) {
// Handles exceptions that arise from match() or fetch().
console.error(' Error in fetch handler:', error);
throw error;
});
})
);
});
五、Notifications API
简介:
Notifications API 允许网页控制向最终用户显示系统通知 —这些都在顶级浏览上下文视口之外,因此即使用户已经切换标签页或移动到不同的应用程序,也可以显示。该API被设计成与不同平台上的现有通知系统兼容。Notification API是浏览器的通知接口,用于在用户的桌面(而不是网页上)显示通知信息,桌面电脑和手机都适用(如通知用户快递状态)。具体的实现形式由浏览器自行部署,对于手机来说,一般显示在顶部的通知栏。
首次使用Notifications API时,浏览器会提示是否允许接收通知,当用户同意后,才允许后续所有操作。目前除IE外,均已经支持该API。
Notifications.permission:
属性 | 描述 |
default | 默认值,此时会询问用户是否接收通知 |
granted | 用户授权允许接收通知 |
denied | 用于拒绝接收通知,此时浏览器不会推送消息 |
示例:
//检查是否支持Notification
if (window.Notification) {
// 支持
} else {
// 不支持
}
//发起通知
if(Notification && Notification.permission !== "denied") {
//请求用户当前来源的权限以显示通知
Notification.requestPermission(function(status) {
/**
* new Notification(title, options) 生成一条消息通知
* title参数是必须的,指定消息头
* options中包含多个属性:
* dir:文字方向,可能的值为auto、ltr(从左到右)和rtl(从右到左),一般是继承浏览器的设置
* lang:语言,如en-US、zh-CN
* body:通知内容,格式为字符串
* tag:通知的ID,格式为字符串。一组相同tag的通知,不会同时显示,只会在用户关闭前
* 一个通知后,在原位置显示。
* icon:图标的链接,显示在通知上
*/
new Notification('标题标题', { body: '通知内容!' });
});
}
/**
* Notification监听方法
* show:通知显示给用户时触发。
* click:用户点击通知时触发
* close:用户关闭通知时触发。
* error:通知出错时触发(大多数发生在通知无法正确显示时)
*/
var notification=new Notification('标题标题', { body: '通知内容!' });
notification.onshow=function(){
//通知显示给用户时触发
}
//Notification支持一个唯一的方法close,能够手动调用,并关闭提示
notification.close();