WEB API权限整理

最近整理一篇关于 一些 浏览器html5 相关api 的权限的东西 虽然大部分都可以用了 但是基本上还是草案居多

本文所有的demo都在 这里 blog-demo/permission 代码在这github.com/thesadabc/blog-demo

有几个权限的demo还没有写好 之后会单独写出来

w3c.github.io/permissions: Editor’s Draft, 17 October 2016

事实上不止链接中的这几个权限, 比如usb, 本文就只对下面列出的这几个权限进行介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
enum PermissionName {
"geolocation",
"notifications",
"push",
"midi",
"camera",
"microphone",
"speaker",
"device-info",
"background-sync",
"bluetooth",
"persistent-storage"
}

push background-sync需要配合service worker工作

notificationsservice worker与浏览器环境中有不同

检查权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
navigator.permissions.query({ 
name: "notifications", // ["notifications", "push", "midi", "background-sync"]
//not support now: ["camera", "microphone", "speaker", "device-info", "bluetooth", "persistent-storage"]
userVisibleOnly: false, // for "push"
sysex: false, // for "midi"
deviceId: null, // for ["camera", "microphone", "speaker"], 可以从`navigator.mediaDevice`接口获取
}).then((permissionStatus) => {
let {
state // ["granted", "denied", "prompt"]
} = permissionStatus;
permissionStatus.onchange = function () {}
});

// 请求权限 目前没有浏览器实现这个接口 所以在各个接口有自己的实现
navigator.permissions.request({})

geolocation

www.w3.org/TR/geolocation-API: W3C Recommendation 8 November 2016

虽然这个版本是2016年11月8日出的第二版 才出没多久 按照其他权限的实现方式 估计这个方案会改成Promise的方式 而且会增加一个专门来请求权限的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 调用的时候会向用户请求权限
navigator.geolocation.getCurrentPosition((coords) => {
let {
timestamp,
coords:{
accuracy,
latitude,
longitude,
altitude,
altitudeAccuracy,
heading, // 0 - 360 从北开始的顺时针
speed
}
} = coords;
}, (err) => {
let {
code, // PERMISSION_DENIED = 1; POSITION_UNAVAILABLE = 2; TIMEOUT = 3;
message
} = err;

console.log(err)
}, {
enableHighAccuracy: false,
timeout: 0xFFFFFFFF,
maximumAge: 0
});

// let watchId = navigator.geolocation.watchPosition(()=>{}, ()=>{}, {})
// navigator.geolocation.clearWatch(watchId)

Notification

notifications.spec.whatwg.org: Living Standard — Last Updated 15 February 2017

通知有两套实现方案 一个是在浏览器中直接调用构造函数生成通知 另外一个是中调用service worker里的serviceWorkerRegistration对象上的构造方法

两者有两个区别

  1. 事件监听方式不同 第二种可以将事件注册在全局对象ServiceWorkerGlobalScope
  2. 通知属性不同 第二种比第一种多了actions属性 可以在通知框上添加额外的按钮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 请求通知权限
Notification.requestPermission().then((perm)=>{
// "default", "denied", "granted"
if (perm === "denied") return log("permission denied")

let noti = new Notification("title", {
dir: "auto", //"auto", "ltr", "rtl"
lang: "", // en
body: "body",
tag: "tag", // 每个通知的标识 如果前一个相同tag的通知未消失 则本通知直接替换前一个 不触发新的事件 如 前一个的`close`事件与当前的`show`事件
image: "", // 资源地址
icon: "", // 资源地址
badge: "", // 资源地址 // 这啥??
sound: null, // 资源地址
vibrate: null, // [100, 50], 震动100ms 暂停50ms

timestamp: 123123, // ???

data: null,
});

let {
renotify, // 该通知是否替换前一个通知
silent, // 是否静音 即本通知无声音和震动
requireInteraction, // 是否可以被用户处理
} = noti;

noti.addEventListener("click", function(e){
// noti.close()
})
noti.addEventListener("error", function(e){
})
noti.addEventListener("show", function(e){
})
noti.addEventListener("close", function(e){
})


// in service worker
self.registration.showNotification("title", {
// ...
actions: [ // 仅限 server worker
{
action: "action1",
title: "actionTitle",
icon: "" // 资源地址
}
]
}).then(() => {});
self.registration.getNotifications({tag:"tag"}).then(([]) => {});
self.addEventListener("notificationclick", function (e){
console.log("notificationclick")
console.log(e.action)
e.notification.close();
});
self.addEventListener("notificationclose", function (e){
console.log("notificationclose")
});
});

push

www.w3.org/TR/push-api/: W3C Working Draft 22 February 2017

相关内容:
appmanifest
Progressive Web Apps

这个api和Progressive Web Apps的推送有关 内容比较复杂 之后我再单独开一篇文章写这个吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// in browser
navigator.serviceWorker.register("worker.js").then((serviceWorkerRegistration) => {

return serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: false,
applicationServerKey: null
});
}).then((pushSubscription) => {
console.log(pushSubscription.endpoint);
console.log(pushSubscription.options);
console.log(pushSubscription.getKey("p256dh"));
console.log(pushSubscription.getKey("auth"));
// return pushSubscription.unsubscribe()

}).catch((error) => {
console.log(error);
});


// in worker.js
self.onpush = ({data}) => {
console.log(data);

data.arrayBuffer().then((ab) => { });
// return data.blob();
// return data.json();
// return data.text()
};
self.onpushsubscriptionchange = ({newSubscription, oldSubscription}) => {}

midi

web midi: W3C Working Draft 17 March 2015

获取设备上的MIDI接口 多用于音频设备(主机上也留有这种接口 给键盘鼠标用的)

这个api太专业了 我这里没有测试的设备 所以没法试这个代码 而且数据请求都是直接传送二进制数据 所以也比较头疼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

navigator.requestMIDIAccess({sysex: true}).then((midiAccess) =>{
let { inputs:inputMap, outputs:outputMap, sysexEnabled } = midiAccess;
midiAccess.onstatechange = ({port}) => {};

/****
* inputMap/outputMap: {
* id1:{
* id1, // unique ID of the port
* manufacturer,
* name,
* type, // ["output", "input"]
* version,
* state, // ["disconnected", "connected"]
* connection, //["open", "closed", "pending"]
* }
* }
**/

outputMap[id1].onstatechange = ({port}) => {
// port === outputMap[id1]
};
inputMap[id1].onstatechange =({port}) => {
// port === inputMap[id1]
};


for(let [id, output] of outputMap.entries()){
output.clear();
output.send(new Uint8Array([ 0x90, 0x45, 0x7f ]), Date.now());
// output.send([ 0x90, 0x45, 0x7f ], Date.now());
}

for(let [id, input] of inputMap.entries()){
input.onmidimessage = ({data, receivedTime}) => {};
}


return inputMap[id1].open().then((port) => {
// port === inputMap[id1]
});
// return inputMap[id1].close();
// return outputMap[id1].close();
// return outputMap[id1].open();
});

media: camera, microphone, speaker, device-info

www.w3.org/TR/mediacapture-streams: W3C Candidate Recommendation 19 May 2016

www.w3.org/TR/audio-output: W3C Working Draft 15 December 2016

主要涉及两个函数

navigator.mediaDevices.enumerateDevices(): 查询设备信息 和device-info权限相关 一般都是允许的

navigator.mediaDevices.getUserMedia(): 获取设备媒体输入数据 对应的cameramicrophone权限 这个函数做过一次相当大的调整 原本是直接在navigator下的navigator.getUserMedia 而且是回调式的 现在是基于Promise的异步

关于speaker权限 暂时还没有去研究 看接口应该是能指定某个音箱设备播放声音 而且有单独的标准audio-output

除了浏览器的接口外 还涉及HTMLMediaElement相关的东西

srcObject属性 设置播放源

1
2
3
video.srcObject = stream;
// or this
video.src = URL.createObjectURL(stream);

sinkId属性 设置播放设备

1
audio.setSinkId(deviceId);

capture 属性 选择摄像头拍摄内容作为文件 移动端较为常见 而且常内置于type="file"标签中

1
<input type="file" accept="video/*, image/*, audio/*" capture>

allowusermedia属性

1
iframe.allowUserMedia = true

MediaStream Image Capture 图像捕获相关的接口

除了这些之外 这个标准还和webrtc标准有一点联系 可以研究一下

标准里涉及到了多个非常非常相似的几个数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
SupportedConstraints { // 各个属性是否支持
width: true,
height: true,
aspectRatio: true,
frameRate: true,
facingMode: true,
volume: true,
sampleRate: true,
sampleSize: true,
echoCancellation: true,
latency: true,
channelCount: true,
deviceId: true,
groupId: true,
};


Settings { // 这三个结构到底是想怎么样啊!!!
width; // 1000
height; // 1000
aspectRatio; // 1000
frameRate; // 1000
facingMode; // string ["user", "environment", "left", "right"]
volume; // 1000
sampleRate; // 1000
sampleSize; // 1000
echoCancellation; // boolean
latency; // 1000
channelCount; // 1000
deviceId; // "string"
groupId; // "string"
};

Capabilities: { // 和ConstraintSet很像
width; // 1000 or {min,max}
height; // 1000 or {min,max}
aspectRatio; // 1000 or {min,max}
frameRate; // 1000 or {min,max}
facingMode; // [string]
volume; // 1000 or {min,max}
sampleRate; // 1000 or {min,max}
sampleSize; // 1000 or {min,max}
echoCancellation; //[boolean]
latency; // false or [boolean]
channelCount;// 1000 or {min,max}
deviceId; // string
groupId; // string
}

ConstraintSet: { // 这个结构太tm复杂了
width: null, // only video, 10000 or {min, max, exact, ideal}
height: null, // only video, 10000 or {min, max, exact, ideal}
aspectRatio: null, // only video, 10000 or {min, max, exact, ideal}
frameRate: null, // only video, 10000 or {min, max, exact, ideal}
facingMode: null, // only video, string or [string] or {exact, ideal}
volume: null, // 10000 or {min, max, exact, ideal}
sampleRate: null, // 10000 or {min, max, exact, ideal}
sampleSize: null, // 10000 or {min, max, exact, ideal}
echoCancellation: null,// boolean or {exact, ideal}
latency: null, // 10000 or {min, max, exact, ideal}
channelCount: null,// 10000 or {min, max, exact, ideal}
deviceId: null, // string or [string] or {exact, ideal}
groupId: null, // string or [string] or {exact, ideal}
}

Constraints:{ // 这个结构也是神了.....
...ConstraintSet
advanced:[ConstraintSet]
}

这三个结构 相当于Settings是最基础的 Capabilities在前者只是给每个属性增加了扩展: 字符串可以使用数组, 数值可以给范围 ConstraintSet 再进行扩展 给每个属性增加了预期值和实际值 Constraints再在前者上增加了一个属性 可以设置为同结构的数组 几个数据结构用的地方也不太一样 要注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// device-info
navigator.mediaDevices.enumerateDevices().then(([device])=>{
let {
deviceId,
groupId, // 同一个物理设备上的设备的值相同
kind, // ["audiooutput", "audioinput", "videoinput"]
label
} = device.toJSON()

// set audio speaker
audioElement.setSinkId(deviceId).then(() => {});
// let did = audioEle.sinkId;

// input only
let capabilities= device.getCapabilities()
});

navigator.mediaDevices.ondevicechange = () => {};

let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
if(!supportedConstraints["width"] || !supports["height"]) {
// if not support width or height
}
navigator.mediaDevices.getUserMedia({
video: { // constraints or true
width: {min: 320, ideal: 1280, max: 1920},
height: {min: 240, ideal: 720, max: 1080},
},
audio: true, // constraints or true
}).then((stream) => {

videoElement.srcObject = stream;

let {id, active} = stream;
let stream2 = stream.clone();
stream.onaddtrack = ({track})=>{};
stream.onremovetrack = ({track})=>{};


let tracksList = stream.getAudioTracks();
// stream.getVideoTracks();
// stream.getTracks();
let track = stream.getTrackById(tracksList[0].id);
stream.removeTrack(track);
stream.addTrack(track);

let {
kind, // ["video", "audio"]
id,
label,
enabled,
muted,
readyState, // ["live", "ended"]
} = track;

track.onended = (e)=>{}
track.onmute = (e)=>{}
track.onunmute = (e)=>{}
let track2 = track.clone()

// only video
let imageCapture = new ImageCapture(track);
// take photo
imageCapture.takePhoto().then(blob => {
image.src = URL.createObjectURL(blob);
});
// draw to cavas
setInterval(() => {
imageCapture.grabFrame().then(frame => {
canvas.width = imgData.width;
canvas.height = imgData.height;
canvas.getContext('2d').drawImage(imgData, 0, 0);
})
}, 1000);

// Constrainable Pattern
track.onoverconstrained = ({error})=>{};
track.applyConstraints(Constraints).then(() => {});

// let capabilities = track.getCapabilities()
// let constraints = track.getConstraints()
// let settings = track.getSettings()
track.stop()

});

background-sync

wicg.github.io/BackgroundSync/spec: Draft Community Group Report, 2 August 2016

permission:

Note: Background sync SHOULD be enabled by default. Having the permission denied is considered an exceptional case.

browserServiceWorker之间进行的相当简单的单向通信 不能传数据

当一个耗时的重要操作还没有完成的时候 如果用户把浏览器关了 就会造成数据丢失什么的 为了解决这个问题可以吧这些操作放到ServiceWorker中进行处理 这样

其实感觉这个权限有点点奇怪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// in browser
navigator.serviceWorker.register("worker.js").then((serviceWorkerRegistration) => {
let syncManager = serviceWorkerRegistration.sync;

syncManager.register("send-chats").then(() => {});
syncManager.getTags().then(([tag1]) => {});
});


// in worker.js
self.addEventListener("sync", function({tag, lastChance}) {
if(tag === "send-chats") {
doImportantThing().catch((err) => {
if(lastChance){
// 本次是最后一次重试
// 可以使用 notifications 来提示用户
}
});
}
});

bluetooth

webbluetoothcg.github.io/web-bluetooth: Draft Community Group Report, 15 February 2017

社区 www.w3.org/community/web-bluetooth

相当复杂 另起一篇 而且没有测试设备 这个月初的时候官方才刚刚出了一个demo 还挺复杂

其实本篇文章就是从这个demo开始的 本来想研究下WebBluetooth 然后涉及到浏览器权限 然后干脆就整理一个吧 于是就写了这篇整理权限的东西

persistent-storage

storage.spec.whatwg.org: Living Standard — Last Updated 23 February 2017

似乎这个权限没有明显的代码实例 只是一个普通的查询用的权限 根据这篇文章的回复persistent-storage 似乎认为这个接口是用来查询浏览器端数据存储是否为永久性的. 如果权限查询结果为true 表明这些存储方式存储的数据 在系统存储空间充足的情况下 不会被浏览器清理掉 具体有哪些存储方式 文档有说明storage.spec.whatwg.org/#infrastructure 包括网络请求的缓存 cookie IndexDB LocalStorage等

一个同源站点关联里一个站点存储单元(site storage units) 每个站点存储单元包含了一个单个的空间(box)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在`persistent-storage`权限为`granted`的情况下 将`站点存储单元`的`存储空间`类型转换为`persistent box`
navigator.storage.persist().then((persisted) => {
if(persisted) {
/* … */
}
});

// 查询`站点存储单元`的`存储空间`类型是否为`persistent box`
navigator.storage.persisted().then((persisted) => {
if(persisted) {
/* … */
}
});

navigator.storage.estimate().then(({quota, usage}) => {
// 总空间, 已使用空间
})