性能评估工具 Lighthouse。
初识 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。
网络层拦截图片 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([                  "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 ) =>  {                      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("你好" )); console .log(insertSpace("hello" )); 
export 关键字 模块以文件来承载,模块内的所有变量外部是无法访问的,需要使用 export 关键字进行输出才可以供外部访问。有以下几种导出方式。
单个变量或函数导出 1 2 3 4 5 6 export  const  a = 1 ;export  const  b = 2 ;export  function  say (  console .log("say!" ); } 
以组对象的方式导出 1 2 3 4 5 6 7 8 const  a = 1 ;const  b = 2 ;function  say (  console .log("say!" ); } export  { a, b, say };
使用 as 对导出的变量重命名 1 2 3 4 5 6 7 8 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 export  default  function  say (  console .log("say!" ); } 
import 关键字 当需要使用模块文件时,要通过 import 关键字进行导入操作,来访问模块文件 export 的值。
按变量名导入 1 2 3 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 export  default  function  say (  console .log("say!" ); } import  s from  "./utils.mjs" ;s(); 
同时导入 default 和其他接口 1 2 3 4 5 6 7 8 9 10 11 const  a = 1 ;const  b = 2 ;export  default  function  say (  console .log("say!" ); } export  { a, b };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> 标签不同属性的加载及运行时机有所不同。
写 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 构造函数 
参数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) 
参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 new  Promise (resolve  =>  resolve("resolve value" ); }).then(value = >{   console .log("onResolved" ,value); }) 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" );    }).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  =>"data" ));Promise .resolve("value" ).then(value  =>  console .log(value); }) 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 音频和视频的捕获
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 > 
视频流的截图
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 > 
视频流下载
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 );        })       .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 > 
语音识别
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 ;        recognition.lang = "cmn-Hans-CN" ;        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 > 
剪切板操作
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> 
网络类型及速度信息
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  => 
网络状态信息
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("网络状态变化:当前网络离线" ); }; 
电池状态信息
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);        console .log("距离电池耗尽还剩(S)"  + battery.dischargingTime);       console .log("电池放点等级:"  + battery.level);       battery.onchargingchange;        battery.onchargingtimechange;        battery.ondischargingtimechange;        battery.onlevelchange;      }   ); } 
设备内存信息
1 2 console .log("当前设备内存大小:"  + navigator.deviceMemory + " GB" );
地理定位
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 ,          maximumAge: 0         }     );   };   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 > 
设备位置
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 入门与实践