PWA 入门与实践

性能评估工具 Lighthouse。

初识 PWA

PWA的组成部分

PWA的布局结构

PWA与应用程序的系统结构对比

各浏览器对 PWA 的支持情况

PWA的生态

Chrome 在 72 版本中,对 Android 平台使用了 Trusted Web Activities(TWA) 和 Digital Asset Links(DAL),将 Web 结合到了应用程序中,并支持发布到 Googleplay 商店。

2018 年 6 月,微软也宣布 PWA 可以基于 UWP 发布到 Microsoft Store 中,作为应用程序使用。

HTTP Server

PWA 必须运行在 HTTPS 环境或者 127.0.0.1 的本地服务环境下,所以在开发、测试的过程中需要有一个本地的 HTTP Server,这里建议使用 http-server,它是一个基于 Node.js 环境的简单、零配置的 HTTP Server 命令行工具。

1
2
// -p 指定端口
http-server -p 9000

调试工具

调试工具建议使用 Chrome 的内置工具 DevTools。

DevTools工具

对Service Worker 进行清除

网络层拦截图片

1
2
3
4
5
6
7
//  拦截请求
self.addEventListener("fetch", (event) => {
if (/network\.jpg$/.test(event.request.url)) {
// 返回响应
return event.respondWith(fetch("images/pwa.jpg"));
}
});

定制 404 页面

1
2
3
4
5
6
7
8
9
10
11
12
13
self.addEventListener("fetch", (event) => {
if (event.request.mode == "navigate") {
return event.respondWith(
fetch(event.request).then((res) => {
// 判断状态
if (res.status == 404) {
return fetch("custom404.html");
}
return res;
})
);
}
});

离线可用

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
const CACHE_NAME = "pwa"; // 定义缓存名称

self.addEventListener("install", (event) => {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then((cache) =>
cache.addAll([
// 在 Service Worker 安装时,将相关资源进行缓存
"images/network.jpg",
"custom404.html",
"/",
"index.html",
])
)
);
});

self.addEventListener("fetch", (event) => {
return event.respondWith(
fetch(event.request)
.then((res) => {
if (event.request.mode == "navigate" && res.status == 404) {
return fetch("custom404.html");
}
return res;
})
.catch(() => {
// 离线状态下的处理
return caches.open(CACHE_NAME).then((cache) => {
// 从 Cache 里面取资源
return cache.match(event.request).then((response) => {
if (response) {
return response;
}
return cache.match("custom404.html");
});
});
})
);
});

self.addEventListener("activate", (event) => {
clients.claim();
});

预备知识

JavaScript Module

1
<script type="module"></script>

当项目变大时,代码也变得难以维护。们直接面临着一系列问题,包括:

  • 命名空间冲突:每一个脚本都暴露在全局作用域下,很可能造成命名冲突,例如 JQuery 和 Zepto 都使用 window.$。
  • 依赖关系不清晰:对于脚本的依赖、版本和加载顺序无法合理地管理。

JavaScript 的模块化发展过程为从无模块化到 Common.js 规范,再到 AMD 规范、CMD 规范、UMD 规范。

Common.js 规范是 Node.js 的模块化规范,核心是通过 require 方法加载模块,通过 module. exports 来导出模块。浏览器无法直接使用,需要依赖于打包工具进行支持,如 webpack。 > AMD 规范与 Common.js 规范的主要区别是异步加载模块。 > CMD 规范和 AMD 规范类似,主要区别是依赖后置,模块加载完再执行。 > UMD 规范则是 AMD 规范和 Common. js 规范的结合。这三种规范在导入相应规范库的前提下,可以直接在浏览器执行。 > 以上模块规范各有优缺点,最终整合为浏览器原生支持的 ES6 Module 规范,也就是本章将要介绍的 JavaScript Module。

JavaScript Module 也称为 ES Module 或 ECMAScript Module。模块化的主要共同点是允许导入和导出模块。之前的几种模块化方案都是社区实现的,并不是 JavaScript 的标准规范,而 JavaScript Module 模块化方案是一个真正的规范,是可以直接运行在浏览器中的。

JavaScript Module 主要使用到了 export 和 import 命令。在模块内,
可以使用export 关键字导出 const、函数或任何其他变量绑定或声明,如 utils.mjs:

1
2
3
4
export const sayLen = (str) => `字符串长度为 ${str.length}`;
export function insertSpace(str) {
return str.split("").join(" ");
}

要使用模块,可以用import 关键字将要使用的模块导入。例如,我们在 index.mjs 中使用 utils.mjs 模块中的 sayLen 和 insertSpace 方法:

1
2
3
4
import { sayLen, insertSpace } from "./utils.mjs";

console.log(sayLen("你好")); // 字符串长度为 2
console.log(insertSpace("hello")); // hello

export 关键字

模块以文件来承载,模块内的所有变量外部是无法访问的,需要使用 export 关键字进行输出才可以供外部访问。有以下几种导出方式。

单个变量或函数导出
1
2
3
4
5
6
//  utils.mjs
export const a = 1;
export const b = 2;
export function say() {
console.log("say!");
}
以组对象的方式导出
1
2
3
4
5
6
7
8
//  utils.mjs
const a = 1;
const b = 2;
function say() {
console.log("say!");
}

export { a, b, say };
使用 as 对导出的变量重命名
1
2
3
4
5
6
7
8
//  utils.mjs
const a = 1;
const b = 2;
function say() {
console.log("say!");
}

export { a as varA, b as varB, say as FnSay };
可以使用 default 默认导出
1
2
3
4
// utils.mjs
export default function say() {
console.log("say!");
}

import 关键字

当需要使用模块文件时,要通过 import 关键字进行导入操作,来访问模块文件 export 的值。

按变量名导入
1
2
3
//  index.mjs
import { a, b, say } from "./utils.mjs";
say();
导入重命名
1
2
import { a, b, say as FnSay } from "./utils.mjs";
FnSay();
可以使用 * 进行全量导入
1
2
3
4
5
import * as utils from "./utils.mjs";

utils.say();
utils.a;
utils.b;
导入 export default 类型,可以随意命名
1
2
3
4
5
6
7
8
// utils.mjs
export default function say() {
console.log("say!");
}

// index.mjs
import s from "./utils.mjs";
s();
同时导入 default 和其他接口
1
2
3
4
5
6
7
8
9
10
11
//  utils.mjs
const a = 1;
const b = 2;
export default function say() {
console.log("say!");
}
export { a, b };

// index.mjs
import say, { a, b } from "./utils.mjs";
import say, * as utils from "./utils.mjs";

JavaScript Module 中对于 import from 的路径有严格要求,必须是完整的 URL 或者以 “/“ “./“ “../“ 开头。

1
2
3
4
5
// 支持
import { each } from "./lodash.mjs";
import utils from "../utils.mjs";
import utils from "/modules/utils.mjs";
import utils from "https://test.test/modules/utils.mjs";
动态 import 方法

按需加载,需要的时候下载。

1
2
3
import("./utils.mjs").then((module) => {
module.say();
});

浏览器中使用 JavaScript Module

1
2
<script type="module" src="index.mjs"></script>
<script nomodule src="index-compatible.js"></script>

上面的代码中,module 主要用来让支持 Module 的浏览器使用 index.mjs、nomodule,让不支持 Module 的浏览器使用兼容的index-compatible.js,同时忽略 type=”module”。

加载时机

浏览器对<script> 标签不同属性的加载及运行时机有所不同。

script加载时机

写 nomodule 的时候建议也加入 defer 属性

1
<script nomodule defer src="index-compatible.js"></script>

扩展名

对于JavaScript Module的脚本,浏览器要求服务器的相应类型必须是JavaScriptMIME type text/javascript,扩展名并不重要。上面我们用.mjs作为JavaScriptModule的扩展名主要有两个原因:

  • mjs 可以让开发者知道这个文件是JavaScript Module,很容易进行区分
  • mjs 的扩展名可以让Node.js 或者 Babel 等默认按照 Module 进行解析,作为Module的交叉兼容方式。

执行次数

JavaScript Module的执行次数与传统的JavaScript也是不同的。相同的JavaScript Module加载完成后只会执行一次,而传统的JavaScript加载几次就执行几次

1
2
3
4
5
6
7
8
9
10
//  a.js  执行多次
<script src="a.js"></script>
<script src="a.js"></script>

// b.mjs 只执行一次
<script type="module" src="b.mjs"></script>
<script type="module" src="b.mjs"></script>
<script type="module">
import './b.mjs'
</script>

跨域

如果JavaScript Module文件存在跨域,需要相应的服务器提供必需的CORSHeader来进行支持,如Access-Control-Allow-Origin: *。

为什么要用 JavaScript Module

JavaScript Module有如下优势:

  • 提供了一种更好的方式来组织变量和函数。
  • 可以把代码分割成更小的、可以独立运行的代码块。
  • 支持更多的现代浏览器语法,书写起来更方便。
  • 支持PWA Service Worker的所有主流浏览器,也支持JavaScriptModule,所以不需要做任何适配旧浏览器的代码转化,直接将代码提供给各个浏览器即可。也就是忽略了不支持的旧浏览器。

Promise

回调方式主要会导致两个关键问题:

  • 嵌套太深导致代码可读性太差。
  • 行逻辑必须串行执行。

Promise 对象用于表示一个异步操作的结果,最终结果可能是“完成”“失败”或者其结果的值。Promise将嵌套的回调改造成一系列使用.then的链式调用,去除了层层嵌套的劣式代码风格。Promise不是一种解决具体问题的算法,而是一种更好的代码组织模式。

Promise对象的特点

  • 对象的状态不受外界影响。 Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)、 Rejected(已失败)。根据异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,表示无法通过其他手段改变对象的状态。

  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved;从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,再对Promise对象添加回调函数,也会立即得到这个结果,

Promise 结果状态

Promise 构造函数

参数executor是一个带有resolve和reject两个参数的函数。executor函数在Promise构造函数执行时同步执行,被传递到resolve和reject函数(executor函数在Promise构造函数返回新建对象前被调用)。resolve和reject函数被调用时,分别将Promise的状态改为fulfilled(完成)或rejected(失败)。executor内部通常会执行一些异步操作,一旦完成,则可以调用resolve函数来将Promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。如果在executor函数中抛出一个错误,那么该Promise状态为rejected。executor函数的返回值被忽略。

1
2
3
4
5
6
7
new Promise((resolve,reject) => {
if(done) {
resolve(value);
}else{
reject(error)
}
})

实例方法

then()
1
promiseObj.then(onResolved,onRejected)

参数:
onResolved:函数类型。用于处理当前 Promise 对象 Resolved 状态的回调,参数为 Resolved 的值。
onRejected:函数类型。可选。用于处理当前 Promise 对象 Rejected 状态的回调,参数 Rejected 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  处理 Resolved 回调
new Promise(resolve =>{
resolve("resolve value");
}).then(value = >{
console.log("onResolved",value);
})

// 处理 onRejected 回调
new Promise((resolve,reject)=>{
reject("reject value");
}).then(
value => {},
value => {
console.log("onRejected",value)
}
)
catch
1
promiseObj.catch(onRejected);

该方法用于添加当前 Promise 对象 Rejected 的状态回调,并返回 Promise 对象的方法。它的行为与调用 promiseObj.then(undefined, onRejected) 相同。

1
2
3
4
5
new Promise((resolve,reject) =>{
reject("reject value");
}).catch(value => {
console.log("onRejected",value);
})
finally
1
promiseObj.finally(onFinally)

该方法用于添加当前Promise对象的状态回调,无论结果是Resolved还是Rejected,都会执行这个回调函数,其返回值为Promise。

1
2
3
4
5
6
new Promise((resolve,reject) => {
resolve("resolve value");
// reject("reject value");
}).finally(()=>{
console.log("finally");
})
关系图
1
2
3
4
5
6
7
8
9
10
taskA().then(
() => taskB(),
() => taskB()
}).then(() => {
taskD()
}).catch(() => {
taskE()
}).finally(()=>{
taskF();
})

实例方法的关系图

静态方法

Promise 接口包含如下静态方法:resolve、reject、all、race。

resolve
1
2
3
Promise.resolve(value)
Promise.resolve(promise)
Promise.resolve(thenable)

该方法返回一个状态由给定value决定的Promise对象。如果该值是一个Promise对象,则直接返回该对象;如果该值是thenable(即带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则(即该value为空、基本类型或者不带then方法的对象),返回的Promise对象状态为Resolved,并且将该value传递给对应的then方法。
有时需要将现有对象转换为Promise对象,Promise.resolve方法就起到这个作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Promise.resolve("data");
// 等同于
new Promise(resolve => resolve("data"));

// value
Promise.resolve("value").then(value => {
console.log(value);
})

// promise
const originPromise = Promise.resolve("originPromise");
Promise.resolve(originPromise).then(value =>{
console.log(value);
})

Promise.resolve({
then:function(onResolved,onReject){
onReject("onReject!");
}
}).then(value =>{
console.log(value);
})
reject

实践方案

系统集成

系统集成项目组Fugu

音频和视频的捕获
可以通过MediaDevices API来实现音频和视频的捕获。这个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
<div>
<button id="btnVi">捕获视频</button>
</div>
<video id="vi" controls width="350" height="200"></video>
<div>
<button id="btnAu">捕获音频</button>
</div>
<audio id="au" controls></audio>
<script>
document.getElementById("btnVi").onclick = () => {
getStream("video", document.getElementById("vi"));
};

document.getElementById("btnAu").onclick = () => {
getStream("audio", document.getElementById("au"));
};
function getStream(type, el) {
if (!navigator.mediaDevices) {
alert("mediaDevices API 不支持");
return;
}
navigator.mediaDevices
.getUserMedia({ [type]: true })
.then(stream => {
if ("srcObject" in el) {
el.srcObject = stream;
} else {
el.src = window.URL.createObjectURL(stream);
}
el.onloadedmetadata = () => {
el.play();
};
})
.catch(err => {
console.log("捕获视频错误:", err);
});
}
</script>

视频流的截图
通过ImageCapture API来控制设备摄像头的高级设置,例如缩放、白平衡、ISO或对焦等,并根据这些设置进行照片生成。

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
<div>
<button id="btnVi">捕获视频</button>
</div>
<video id="vi" controls width="350" height="200"></video>
<div>
<button id="btnPhoto">视频截图</button>
</div>
<img id="photo" style="border: 1px solid #aaa;width:350px;height:200px;" />
<script>
let vStream;

document.getElementById("btnVi").onclick = () => {
getStream("video", document.getElementById("vi"));
};

document.getElementById("btnPhoto").onclick = () => {
takePhoto(vStream);
};

function getStream(type, el) {
if (!navigator.mediaDevices) {
alert("mediaDevices API 不支持");
return;
}
navigator.mediaDevices
.getUserMedia({ [type]: true })
.then(stream => {
vStream = stream;
if ("srcObject" in el) {
el.srcObject = stream;
} else {
el.src = window.URL.createObjectURL(stream);
}
el.onloadedmetadata = () => {
el.play();
};
})
.catch(err => {
console.log("捕获视频错误:", err);
});
}

function takePhoto(stream) {
if (!stream) {
alert("请先进行视频捕获。");
return;
}
if (!("ImageCapture" in window)) {
alert("ImageCapture API 不支持。");
return;
}

new ImageCapture(stream.getVideoTracks()[0])
.takePhoto()
.then(data => {
document.getElementById("photo").src = URL.createObjectURL(data);
})
.catch(err => console.log("截图错误: ", err));
}
</script>

视频流下载
对于视频录制,很多场景下需要提供下载录制的视频的功能。这里可以借助MediaRecorder 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
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
<div>
<button id="btnVi">捕获视频 & 开始记录</button>
</div>
<video id="vi" controls width="350" height="200"></video>
<div>
<button id="btnPhoto">下载</button>
</div>
<script type="module">
let vStream;
let vRecorder;
let recorderData = [];

document.getElementById("btnVi").onclick = () => {
getStream(document.getElementById("vi"));
};
document.getElementById("btnPhoto").onclick = () => {
download();
};

function getStream(el) {
if (!navigator.mediaDevices) {
alert("mediaDevices API 不支持");
return;
}

navigator.mediaDevices
.getUserMedia({ video: true, audio: true })
.then(stream => {
vStream = stream;
if ("srcObject" in el) {
el.srcObject = stream;
} else {
el.src = window.URL.createObjectURL(stream);
}
el.onloadedmetadata = () => {
el.play();
};

try {
vRecorder = new MediaRecorder(stream, { mimeType: "video/webm" });
console.log("创建 MediaRecorder: ", vRecorder);
} catch (e) {
return console.error("创建 MediaRecorder 失败:", e);
}

vRecorder.ondataavailable = e => {
if (e.data.size == 0) {
return;
}
recorderData.push(event.data);
};
vRecorder.start(100); // 设置 ondataavailable 的触发间隔
})
.catch(err => {
console.log("捕获视频错误:", err);
});
}

function download() {
if (!vStream || !vRecorder) {
alert("请先捕获视频");
return;
}
console.log("开始下载");
vRecorder.stop();
vStream.getTracks()[0].stop();
vStream.getVideoTracks()[0].stop();

const aDom = document.createElement("a");
document.body.appendChild(aDom);
aDom.style = "display: none";
aDom.href = URL.createObjectURL(
new Blob(recorderData, { type: "video/webm" })
);
aDom.download = "download.webm";
aDom.click();

recorderData = [];
vStream = vRecorder = null;
}
</script>

语音识别
Web目前提供的语音识别相关的API,可以实现语音的获取及识别能力,这也是Web的另一种常用的输入能力。这里主要使用SpeechRecognition 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<div style="border:1px solid #ccc; width: 350px; height: 200px; ">
<span id="content-final"></span>
<span id="content-tmp" style="color:gray"></span>
</div>
<div>
<button id="btn">开始识别</button>
</div>
<script type="module">
let btnDom = document.getElementById("btn");
let contentFinalDom = document.getElementById("content-final");
let contentTmpDom = document.getElementById("content-tmp");
let recognition;

btnDom.onclick = () => {
window.SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;

if (!SpeechRecognition) {
alert("不支持 SpeechRecognition API");
return;
}

if (btnDom.innerText === "开始识别") {
recognition = new SpeechRecognition();
recognition.continuous = true; // 边说边识别
recognition.interimResults = true; // 临时识别的结果也显示。通过 isFinal 来确定
recognition.lang = "cmn-Hans-CN"; // 中文普通话。遵循 BCP-47 规范
recognition.start();
btnDom.innerText = "停止识别";
recognition.onstart = () => {
contentFinalDom.innerText = "";
contentTmpDom.innerText = "";
console.log("识别开始");
};
recognition.onresult = event => {
console.log("识别中", event.results);
let content = "";
let contentTmp = "";
for (let i = 0; i < event.results.length; i++) {
if (event.results[i].isFinal) {
content += event.results[i][0].transcript;
} else {
contentTmp += event.results[i][0].transcript;
}
}
contentFinalDom.innerText = content;
contentTmpDom.innerText = contentTmp;
};
recognition.onerror = event => {
console.log("识别错误", event);
};
recognition.onend = () => {
console.log("识别结束");
};
return;
}

recognition.stop();
recognition = null;
btnDom.innerText = "开始识别";
};
</script>

剪切板操作
在传统的Web能力中是不允许读取剪切板的,但目前有了Clipboard API,我们可以通过这个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
46
47
<input id="copy" value="这是一段文字" />
<div>
<button id="btnCopy">复制</button>
</div>
<input id="paster" />
<div>
<button id="btnPaster">粘贴</button>
</div>

<script type="module">
let btnCopyDom = document.getElementById("btnCopy");
let btnPasterDom = document.getElementById("btnPaster");
let copyValueDom = document.getElementById("copy");
let pasterValueDom = document.getElementById("paster");

btnCopyDom.onclick = () => {
if (!"clipboard" in navigator) {
alert("不支持 clipboard API");
return;
}

navigator.clipboard
.writeText(copyValueDom.value)
.then(() => {
console.log(`复制 ${copyValueDom.value} 成功`);
})
.catch(err => {
console.error(`复制失败`, err);
});
};

btnPasterDom.onclick = () => {
if (!"clipboard" in navigator) {
alert("不支持 clipboard API");
return;
}
navigator.clipboard
.readText()
.then(e => {
pasterValueDom.value = e;
console.log(`粘贴 ${e} 成功`);
})
.catch(err => {
console.error(`粘贴失败`, err);
});
};
</script>

网络类型及速度信息
可以通过Network Information API获取设备网络相关信息,开发者可以根据这些网络信息进行定制化处理。

1
2
3
4
5
6
7
8
9
10
if (!navigator.connection) {
console.log("不支持 Network Information API");
return;
}

console.log("底层连接类型:" + navigator.connection.type);
console.log("有效连接类型:" + navigator.connection.effectiveType);
console.log("最大下行速度(MB):" + navigator.connection.downlinkMax);

navigator.connection.onchange = info => { }; // 网络信息发生变化时触发

网络状态信息
可以通过navigator.onLine及一些在线、离线事件来监听网络变化,根据网络变化来做一些用户交互。响应事件代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
if (navigator.onLine) {
console.log("你的网络当前在线");
} else {
console.log("你的网络当前离线");
}

window.ononline = () => {
console.log("网络状态变化:当前网络在线");
};

window.onoffline = () => {
console.log("网络状态变化:当前网络离线");
};

电池状态信息
可以通过BatteryManager API来获取设备的电池状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (!navigator.getBattery || !navigator.battery) {
console.log("不支持 BatteryManager API");
} else {
(navigator.getBattery() || Promise.resolve(navigator.battery)).then(
battery => {
console.log("当前电池充电:" + battery.charging);
console.log("距离充电完成还剩(S):" + battery.chargingTime); // 0 为充电完成
console.log("距离电池耗尽还剩(S)" + battery.dischargingTime);
console.log("电池放点等级:" + battery.level);

battery.onchargingchange; //电池充电状态更新时被调用。
battery.onchargingtimechange; // 电池充电时间更新时被调用。
battery.ondischargingtimechange; //电池断开充电时间更新时被调用。
battery.onlevelchange; //电池电量更新时被调用。
}
);
}

设备内存信息
可以通过deviceMemory API来获取设备的内存信息,根据内存信息来调整性能,提升各个端的体验。

1
2
console.log("当前设备内存大小:" + navigator.deviceMemory + " GB");
// 当前设备内存大小:8 GB

地理定位
在Web中可以通过Geolocation API获取位置数据,通常它会基于GPS和网络进行定位。

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
<div id="content" style="width: 420px; height: 200px;border: 1px solid #333;"></div>
<div>
<button id="btnGet">获取位置</button>
<button id="btnWatch">监听位置变化</button>
</div>
<script type="module">
let btnGet = document.getElementById("btnGet");
let btnWatch = document.getElementById("btnWatch");
let contentDom = document.getElementById("content");
let watcher;

btnGet.onclick = () => {
if (!"geolocation" in navigator) {
alert("不支持 Geolocation API");
return;
}

navigator.geolocation.getCurrentPosition(
info => {
console.log("获取位置成功", info);
contentDom.innerText += `获取位置:\n纬度 ${info.coords.latitude} 经度 ${info.coords.longitude}\n`;
},
err => {
console.log("获取位置错误", err);
},
{
enableHighAccuracy: false, // 低精度,获取速度快
timeout: Infinity, // 设备必须在多长时间内获取值 ms
maximumAge: 0 // 定位信息的缓存时间 ms
}
);
};

btnWatch.onclick = () => {
if (!"geolocation" in navigator) {
alert("不支持 Geolocation API");
return;
}

if (btnWatch.innerText == "监听位置变化") {
if (watcher) {
return;
}
watcher = navigator.geolocation.watchPosition(
info => {
console.log("监听位置变化:", info);
contentDom.innerText += `监听位置变化:\n纬度 ${info.coords.latitude} 经度 ${info.coords.longitude}\n`;
},
err => {
console.log("监听位置变化错误", err);
},
{}
);
btnWatch.innerText = "停止监听位置变化";
return;
}

watcher && navigator.geolocation.clearWatch(watcher);
contentDom.innerText += "监听停止\n";
console.log("监听停止");
btnWatch.innerText = "监听位置变化";
};
</script>

设备位置
在Web中可以使用Device Orientation API来实现获取陀螺仪、指南针等数据,也可以通过Generic Sensor API和Orientation Sensor API来获取设备方向数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
//  绝对位置
let aos = new AbsoluteOrientationSensor();
// 创建一个相对于地球的参考坐标系统的设备的物流方向的对象
aos.addEventListener("reading",listener); // 方向发生变化时的事件
aos.start(); // 开始监听
aos.quaternion; // 获取方向信息

// 相对位置
let ros = new RelativeOrientationSensor();
// 创建一个相对固定参考坐标系统的设备物理方向的对象
ros.addEventListener('reading',listener); // 方向发生变化时的事件
aos.start(); // 开始监听
aos.quaternion; // 获取方向信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  <img id="img" src="img.png" />
<div id="content"></div>

<script type="module">
let img = document.getElementById("img");
let content = document.getElementById("content");

window.ondeviceorientation = e => {
let { gamma, beta, alpha } = e;
console.log(alpha, beta, gamma);

img.style.transform = `rotate(${alpha}deg) rotate3d(1, 0, 0, ${beta}deg)`;
content.innerHTML = `
alpha(方向): ${alpha} deg<br />
beta(前后): ${beta} deg<br />
gamma(左右): ${gamma} deg
`;
};
</script>

相关资料
FetchEvent.respondWith
Request.mode
ES6 模块化的时代真的来临了么?Using MJS


源码
PWA 入门与实践

作者

Fallen-down

发布于

2020-10-17

更新于

2020-12-21

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.