专业编程基础技术教程

网站首页 > 基础教程 正文

2w+字长文:2024年 PWA 不温不火,盘点 40+逆天特性

ccvgpt 2024-11-08 10:56:45 基础教程 6 ℃

家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

最近读到一篇关于 PWA 的文章《WHAT WEB CAN DO TODAY?》,加上本身自己对 PWA 这个专题也比较感兴趣,所以抽空梳理了 PWA 目前主流功能以及功能描述。

2w+字长文:2024年 PWA 不温不火,盘点 40+逆天特性

文章从用户体验、Native 行为拉齐、App 生命周期、PWA 周边功能、Camera & Microphone、设备特征、操作系统、用户 Input 输入、屏幕和输出等众多维度描述了 PWA 特征。

1. PWA 与无缝体验

离线模式

Web 应用程序可以使用两种技术提供离线体验,较旧的实现,即应用程序缓存(Application Cache)已在浏览器中广泛实现,但由于各种概念和设计缺陷,现在正在弃用。

现代替代方案称为 Cache API,可在 Service Worker 中使用 ,在 HTTPS 上运行的 Web 应用程序可以请求浏览器安装的独立代码单元。 然后,该单元与所属的 Web 应用程序分开运行,并通过事件与其进行通信。 Service Worker 是渐进式 Web 应用程序 (PWA) 理念的基本构建,除了作为推送通知 (Push Notifications) 、后台同步(Background Sync)或地理围栏(Geofencing)等多个复杂 API 的推动者之外,还可以用作功能齐全的 Web 代理。 其可以拦截所有 HTTP 请求,改变其内容或行为甚至管理离线缓存。

navigator.serviceWorker.register(path)
navigator.serviceWorker.ready
serviceWorkerRegistration.update()
serviceWorkerRegistration.unregister()

Background Sync API

Background Sync API 允许授权的 Web 应用程序不依赖于稳定的互联网连接,并将 Web 相关操作推迟到网络连接可用时。 API 绑定到 Service Worker,它是与所属 Web 应用程序分离的代码执行模型,允许后台同步在应用程序窗口关闭后也可以运行。

Background Sync API 本身只是向应用程序发出有关已恢复连接的信号的一种方式。 它可以与任何离线存储解决方案一起使用,以实现数据同步方案或应用程序离线时发出的网络请求的重放机制。

serviceWorkerRegistration.sync.register('syncTag')
self.addEventListener('sync', listener)

Payment Request API

Payment Request API 允许 Web 应用程序将付款结帐流程委托给操作系统,从而允许其使用平台本机可用并为用户配置的任何方法和付款提供商。 这种方法消除了应用程序端处理复杂结账流程的负担,缩小了支付提供商集成的范围,并确保用户更好地熟悉。

const request = new PaymentRequest(
  buildSupportedPaymentMethodData(),
  buildShoppingCartDetails(),
);
function buildSupportedPaymentMethodData() {
  return [{supportedMethods: "https://example.com/pay"}];
}

function buildShoppingCartDetails() {
  return {
    id: "order-123",
    displayItems: [
      {
        label: "Example item",
        amount: {currency: "USD", value: "1.00"},
      },
    ],
    total: {
      label: "Total",
      amount: {currency: "USD", value: "1.00"},
    },
  };
}

Credential Management API

Credential Management API 允许授权的 Web 应用程序代表用户以编程方式存储和请求用户凭证(例如:登录名和密码或联合登录数据)。 该 API 提供了浏览器内置或第三方密码存储的替代方案,允许 Web 应用程序检测何时以及如何存储和读取凭证,例如:提供自动登录功能。

function storeCredential() {
  event.preventDefault();
  if (!navigator.credentials) {
    alert('Credential Management API not supported');
    return;
  }
  let credentialForm = document.getElementById('credential-form');
  let credential = new PasswordCredential(credentialForm);
  //  创建证书
  navigator.credentials.store(credential)
    .then(() => log('Storing credential for' + credential.id + '(result cannot be checked by the website).'))
    .catch((err) => log('Error storing credentials:' + err));
}

function requestCredential() {
  if (!navigator.credentials) {
    alert('Credential Management API not supported');
    return;
  }

  let mediationValue = document.getElementById('credential-form').mediation.value;
  navigator.credentials.get({password: true, mediation: mediationValue})
    .then(credential => {
      let result = 'none';
      if (credential) {
        result = credential.id + ',' + credential.password.replace(/./g, '*');
      }
      log('Credential read:' + result + '');
    })
    .catch((err) => log('Error reading credentials:' + err));
}

function preventSilentAccess() {
  if (!navigator.credentials) {
    alert('Credential Management API not supported');
    return;
  }
  navigator.credentials.preventSilentAccess()
    .then(() => log('Silent access prevented (mediation will be required for next credentials.get() call).'))
    .catch((err) => log('Error preventing silent access:' + err));
}
function waitForSms() {
  if ('OTPCredential' in window) {
    log('Waiting for SMS. Try sending yourself a following message:\n\n' +
        'Your verification code is: 123ABC\n\n' +
        '@whatwebcando.today #123ABC');

    navigator.credentials.get({otp: {transport: ['sms']}})
      .then((code) => log('Code received:' + code))
      .catch((error) => log('SMS receiving error:' + error));
  } else {
    alert('Web OTP API not supported');
  }
}
function log(info) {
  var logTarget = document.getElementById('result');
  var timeBadge = new Date().toTimeString().split(' ')[0];
  var newInfo = document.createElement('p');
  newInfo.innerHTML = ''+ timeBadge +' ' + info;
  logTarget.appendChild(newInfo);
}

2.PWA 与 Native Behaviors

Local Notifications

通过 Notifications API 提供的通知,允许授权的 Web 应用程序以标准化的方式吸引用户的注意力。 通知由在浏览器选项卡中运行的 Web 应用程序生成,并呈现给浏览器选项卡区域之外的用户。

Notification.requestPermission([callback])
Notification.permission
new Notification(title, [options])
navigator.serviceWorker.getRegistration()
.then((reg) => reg.showNotification(title, [options]))

Push Messages

Push Messages 是移动平台上众所周知的功能,其允许授权的 Web 应用程序向用户订阅远程服务器发送的消息,即使 Web 应用程序当前没有聚焦在浏览器中,这些消息也可以触发向订阅者显示通知。 该消息可以传送加密的有效 payload,并且可以请求显示自定义操作按钮。

serviceWorkerRegistration.pushManager.subscribe()
serviceWorkerRegistration.pushManager.getSubscription()
serviceWorker.addEventListener('push', listener)

Foreground Detection

Page Visibility API 对于 Web 应用程序了解当前是否显示在前台非常有用,特别是在不需要时停止资源密集型 UI 动画或数据刷新。 而在移动设备上,这样做的主要原因是减少电池的使用。

var target = document.getElementById('target');

var hidden, visibilityChange;
if (typeof document.hidden !== "undefined") {
  hidden = "hidden";
  visibilityChange = "visibilitychange";
} else if (typeof document.mozHidden !== "undefined") {
  hidden = "mozHidden";
  visibilityChange = "mozvisibilitychange";
} else if (typeof document.msHidden !== "undefined") {
  hidden = "msHidden";
  visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
  hidden = "webkitHidden";
  visibilityChange = "webkitvisibilitychange";
} else {
  target.innerText = 'Page Visibility API not supported.';
}
function handleVisibilityChange() {
  var timeBadge = new Date().toTimeString().split(' ')[0];
  var newState = document.createElement('p');
  newState.innerHTML = ''+ timeBadge +' Page visibility changed to '+ (document[hidden] ?'hidden':'visible') +'.';
  target.appendChild(newState);
}
document.addEventListener(visibilityChange, handleVisibilityChange, false);
if (hidden in document) {
  document.getElementById('status').innerHTML = document[hidden] ? 'hidden' : 'visible';
}

User Idle Detection

User Idle Detection API 允许 Web 应用程序检测用户不活动时的状态,即系统中没有生成用户驱动的事件或屏幕被锁定。 与之前的前台检测功能相反,此 API 不依赖于当前选项卡活动 ,其会检测用户何时离开设备但未锁定设备或已变为非活动状态,无论哪个选项卡处于活动状态。

const idleDetector = new IdleDetector(options)
idleDetector.start()
const state = idleDetector.state
idleDetector.addEventListener('change', listener)

Permissions API

Permissions API 为 Web 应用程序提供了统一的方式来查询可能需要用户同意的功能(如通知或地理位置)的权限状态。 通过 Permissions API,应用程序可以列出用户授予的权限,而无需实际触发该功能本身。

if ('permissions' in navigator) {
  var logTarget = document.getElementById('logTarget');
  function handleChange(permissionName, newState) {
    var timeBadge = new Date().toTimeString().split(' ')[0];
    var newStateInfo = document.createElement('p');
    newStateInfo.innerHTML = ''+ timeBadge +' State of '+ permissionName +' permission status changed to '+ newState +'.';
    logTarget.appendChild(newStateInfo);
  }

  function checkPermission(permissionName, descriptor) {
    try {
    navigator.permissions.query(Object.assign({name: permissionName}, descriptor))
      .then(function (permission) {
        document.getElementById(permissionName + '-status').innerHTML = permission.state;
        permission.addEventListener('change', function (e) {
          document.getElementById(permissionName + '-status').innerHTML = permission.state;
          handleChange(permissionName, permission.state);
        });
      });
    } catch (e) {
    }
  }

  checkPermission('geolocation');
  checkPermission('notifications');
  checkPermission('push', {userVisibleOnly: true});
  checkPermission('midi', {sysex: true});
  checkPermission('camera');
  checkPermission('microphone');
  checkPermission('background-sync');
  checkPermission('ambient-light-sensor');
  checkPermission('accelerometer');
  checkPermission('gyroscope');
  checkPermission('magnetometer');

  var noop = function () {};
  navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);

  function requestGeolocation() {
    navigator.geolocation.getCurrentPosition(noop);
  }

  function requestNotifications() {
    Notification.requestPermission();
  }

  function requestPush() {
    navigator.serviceWorker.getRegistration()
      .then(function (serviceWorkerRegistration) {
        serviceWorkerRegistration.pushManager.subscribe();
      });
  }

  function requestMidi() {
    navigator.requestMIDIAccess({sysex: true});
  }

  function requestCamera() {
    navigator.getUserMedia({video: true}, noop, noop)
  }

  function requestMicrophone() {
    navigator.getUserMedia({audio: true}, noop, noop)
  }
}

Task API

第一个提案称为定期后台同步 API,它解决了后台数据同步用例,补充了后台同步功能。 其允许 Web 应用程序注册周期性事件,从而唤醒 Service Worker,并无需用户交互即可执行 HTTP 请求。

截至 2020 年初,该 API 仅在 Google Chrome 80+ 中进行实验性使用,并且其使用仅限于具有足够高参与度的已安装应用程序。 API 不保证同步的间隔 , 但允许通过 minInterval 参数请求最小间隔,可为了避免滥用,实际间隔取决于网络可信度和用户使用应用程序的频率等诸多因素。

function scheduleNotification() {
  if (!('Notification' in window)) {
    alert('Notification API not supported');
    return;
  }
  if (!('showTrigger' in Notification.prototype)) {
    alert('Notification Trigger API not supported');
    return;
  }
  Notification.requestPermission()
    .then(() => {
      if (Notification.permission !== 'granted') {
        throw 'Notification permission is not granted';
      }
    })
    .then(() => navigator.serviceWorker.getRegistration())
    .then((reg) => {
      reg.showNotification("Hi there from the past!", {
          showTrigger: new TimestampTrigger(new Date().getTime() + 10 * 1000)
      })
    })
    .catch((err) => {
      alert('Notification Trigger API error:' + err);
    });
}

3.PWA 与 App Lifecycle

Home Screen Installation

Web 应用程序可以通过提供 manifest.json 文件标准化为 Web Manifest,指定将应用程序视为目标平台上的一等公民所需的功能和行为,即添加(“install”)到主屏幕, 具有相关图标、全屏行为、主题、无浏览器栏的独立外观等,同时还可以作为放置与 Web 应用程序关联的所有元数据的集中位置。

{
  "short_name": "Example App",
  "name": "The Example Application",
  "icons": [
    {
      "src": "launcher-icon-1x.png",
      "sizes": "48x48"
    },
    {
      "src": "launcher-icon-2x.png",
      "sizes": "96x96"
    }
  ],
  "theme_color": "#ff0000",
  "background_color": "#ff0000",
  "start_url": "index.html",
  "display": "standalone"
}

Freeze/Resume Detection

Page Lifecycle API 是对先前存在的页面状态更改事件的补充,包括:前台检测和焦点信息。 当非活动应用程序的选项卡(inactive application's tab )将被冻结以优化 CPU 和电池使用以及在后续激活时恢复时,其允许 Web 应用程序注册浏览器生成的事件。

该 API 还提供了 wasDiscarded 标志,可以检测冻结选项卡已被丢弃(从内存中删除)并在恢复时需要加载新页面的情况。 对于这种页面加载,该标志将设置为 true。

截至 2020 年春季,该 API 仅在基于 Chromium 的浏览器中实现。

var target = document.getElementById('target');

if ('wasDiscarded' in document) {
  document.getElementById('wasDiscarded').innerText = document.wasDiscarded.toString();
}
function getState() {
  if (document.visibilityState === 'hidden') {
    return 'hidden';
  }
  if (document.hasFocus()) {
    return 'focused';
  }
  return 'not focused';
};

var state = getState();
function logStateChange(nextState) {
  var prevState = state;
  if (nextState !== prevState) {
    var timeBadge = new Date().toTimeString().split(' ')[0];
    var newLog = document.createElement('p');
    newLog.innerHTML = ''+ timeBadge +' State changed from '+ prevState +' to '+ nextState +'.';
    target.appendChild(newLog);
    state = nextState;
  }
};

function onPageStateChange() {
  logStateChange(getState())
}

['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach(function (type) {
  window.addEventListener(type, onPageStateChange, {capture: true});
});

function onFreeze() {
  logStateChange('frozen');
}

window.addEventListener('freeze', onFreeze, {capture: true});

function onPageHide(event) {
  if (event.persisted) {
    // If the event's persisted property is `true` the page is about
    // to enter the page navigation cache, which is also in the frozen state.
    logStateChange('frozen');
  } else {
    // If the event's persisted property is not `true` the page is about to be unloaded.
    logStateChange('terminated');
  }
}

window.addEventListener('pagehide', onPageHide, {capture: true});

4.PWA 与 Surroundings

Bluetooth

Web Bluetooth API 是一个底层 API,允许 Web 应用程序与附近支持低功耗蓝牙的外围设备配对并访问其公开的服务。

function readBatteryLevel() {
  var $target = document.getElementById('target');
  if (!('bluetooth' in navigator)) {
    $target.innerText = 'Bluetooth API not supported.';
    return;
  }
  navigator.bluetooth.requestDevice({
      filters: [{
        services: ['battery_service']
      }]
    })
    .then(function (device) {
      return device.gatt.connect();
    })
    .then(function (server) {
      return server.getPrimaryService('battery_service');
    })
    .then(function (service) {
      return service.getCharacteristic('battery_level');
    })
    .then(function (characteristic) {
      return characteristic.readValue();
    })
    .then(function (value) {
      $target.innerHTML = 'Battery percentage is' + value.getUint8(0) + '.';
    })
    .catch(function (error) {
      $target.innerText = error;
    });
}

USB

WebUSB API 允许 Web 应用程序与系统中可用的通用串行总线兼容设备(Universal Serial Bus-compatible devices )进行交互。 为了授权应用程序访问设备,用户需要在浏览器的 UI 中确认意图,而该意图只能通过手势启动(例如,单击按钮,但不能通过任意 JavaScript 自动启动)。

document.getElementById('arduinoButton').addEventListener('click', function () {
  if (navigator.usb) {
    talkToArduino();
  } else {
    alert('WebUSB not supported.');
  }
});

async function talkToArduino() {
  try {
    let device = await navigator.usb.requestDevice({filters: [{ vendorId: 0x2341}] });
    await device.open();
    await device.selectConfiguration(1);
    await device.claimInterface(2);
    await device.controlTransferOut({
      requestType: 'class',
      recipient: 'interface',
      request: 0x22,
      value: 0x01,
      index: 0x02
    });

    // Ready to receive data
    let result = device.transferIn(5, 64); // Waiting for 64 bytes of data from endpoint #5.
    let decoder = new TextDecoder();
    document.getElementById('target').innerHTML = 'Received:' + decoder.decode(result.data);
  } catch (error) {
    document.getElementById('target').innerHTML = error;
  }
}

Web Serial API

Web Serial API 允许 Web 应用程序与通过串行端口(Serial Port)连接到系统的设备进行交互。 为了授权应用程序访问设备,用户需要在浏览器的 UI 中确认意图,而该意图只能通过手势启动(例如,单击按钮,但不能通过任意 JavaScript 自动启动)。 API 通过一对流公开连接 , 一个用于读取,一个用于写入 Serial Port。

document.getElementById('connectButton').addEventListener('click', () => {
  if (navigator.serial) {
    connectSerial();
  } else {
    alert('Web Serial API not supported.');
  }
});
async function connectSerial() {
  const log = document.getElementById('target');
  try {
    const port = await navigator.serial.requestPort();
    await port.open({baudRate: 9600});

    const decoder = new TextDecoderStream();

    port.readable.pipeTo(decoder.writable);

    const inputStream = decoder.readable;
    const reader = inputStream.getReader();

    while (true) {
      const {value, done} = await reader.read();
      if (value) {
        log.textContent += value + '\n';
      }
      if (done) {
        console.log('[readLoop] DONE', done);
        reader.releaseLock();
        break;
      }
    }

  } catch (error) {
    log.innerHTML = error;
  }
}

5.PWA 与 Camera & Microphone

Audio & Video Capture

Audio & Video Capture API 允许授权的 Web 应用程序访问来自设备的音频和视频捕获接口的流,包括用来自摄像头和麦克风的可用数据。 API 公开的流可以直接绑定到 HTML <audio> 或 <video> 元素,或者在代码中读取和操作,包括通过 Image Capture API, Media Recorder API 或者 Real-Time Communication。

function getUserMedia(constraints) {
  if (navigator.mediaDevices) {
    return navigator.mediaDevices.getUserMedia(constraints);
  }
  var legacyApi = navigator.getUserMedia || navigator.webkitGetUserMedia ||
    navigator.mozGetUserMedia || navigator.msGetUserMedia;
  if (legacyApi) {
    return new Promise(function (resolve, reject) {
      legacyApi.bind(navigator)(constraints, resolve, reject);
    });
  }
}

function getStream (type) {
  if (!navigator.mediaDevices && !navigator.getUserMedia && !navigator.webkitGetUserMedia &&
    !navigator.mozGetUserMedia && !navigator.msGetUserMedia) {
    alert('User Media API not supported.');
    return;
  }
  var constraints = {};
  constraints[type] = true;
  getUserMedia(constraints)
    .then(function (stream) {
      var mediaControl = document.querySelector(type);
      if ('srcObject' in mediaControl) {
        mediaControl.srcObject = stream;
      } else if (navigator.mozGetUserMedia) {
        mediaControl.mozSrcObject = stream;
      } else {
        mediaControl.src = (window.URL || window.webkitURL).createObjectURL(stream);
      }

      mediaControl.play();
    })
    .catch(function (err) {
      alert('Error:' + err);
    });
}

Advanced Camera Controls

Image Capture API 允许 Web 应用程序控制设备相机的高级设置,例如: 变焦、白平衡、ISO 或焦点,并根据这些设置拍照,其依赖于可能从流中获取的 streamVideoTrack 对象 。

function getUserMedia(options, successCallback, failureCallback) {
  var api = navigator.getUserMedia || navigator.webkitGetUserMedia ||
    navigator.mozGetUserMedia || navigator.msGetUserMedia;
  if (api) {
    return api.bind(navigator)(options, successCallback, failureCallback);
  }
}

var theStream;

function getStream() {
  if (!navigator.getUserMedia && !navigator.webkitGetUserMedia &&
    !navigator.mozGetUserMedia && !navigator.msGetUserMedia) {
    alert('User Media API not supported.');
    return;
  }

  var constraints = {
    video: true
  };

  getUserMedia(constraints, function (stream) {
    var mediaControl = document.querySelector('video');
    if ('srcObject' in mediaControl) {
      mediaControl.srcObject = stream;
    } else if (navigator.mozGetUserMedia) {
      mediaControl.mozSrcObject = stream;
    } else {
      mediaControl.src = (window.URL || window.webkitURL).createObjectURL(stream);
    }
    theStream = stream;
  }, function (err) {
    alert('Error:' + err);
  });
}

function takePhoto() {
  if (!('ImageCapture' in window)) {
    alert('ImageCapture is not available');
    return;
  }

  if (!theStream) {
    alert('Grab the video stream first!');
    return;
  }

  var theImageCapturer = new ImageCapture(theStream.getVideoTracks()[0]);

  theImageCapturer.takePhoto()
    .then(blob => {
      var theImageTag = document.getElementById("imageTag");
      theImageTag.src = URL.createObjectURL(blob);
    })
    .catch(err => alert('Error:' + err));
}

Recording Media

Media Recorder API 是一个 Web API,允许 Web 应用程序录制本地或远程音频和视频媒体流,它依赖于 mediaStream 对象 。

recorder = new MediaRecorder(mediaStream, options)
MediaRecorder.isMimeTypeSupported(mimeType)
recorder.start(interval)

Real-Time Communication

Web 中的实时通信(简称 WebRTC)是一组 API,允许 Web 应用程序向远程对等方发送和接收流式实时视频、音频和数据,而无需通过集中式服务器进行依赖。 不过,初始发现和连接握手需要实现特定信令协议之一的服务器。 API 依赖于 mediaStream 对象 。

function getUserMedia(options, successCallback, failureCallback) {
  var api = navigator.getUserMedia || navigator.webkitGetUserMedia ||
    navigator.mozGetUserMedia || navigator.msGetUserMedia;
  if (api) {
    return api.bind(navigator)(options, successCallback, failureCallback);
  }
}
var pc1;
var pc2;
var theStreamB;

function getStream() {
  if (!navigator.getUserMedia && !navigator.webkitGetUserMedia &&
    !navigator.mozGetUserMedia && !navigator.msGetUserMedia) {
    alert('User Media API not supported.');
    return;
  }
  var constraints = {
    video: true
  };
  getUserMedia(constraints, function (stream) {
    addStreamToVideoTag(stream, 'localVideo');
    // RTCPeerConnection is prefixed in Blink-based browsers.
    window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection;
    pc1 = new RTCPeerConnection(null);
    pc1.addStream(stream);
    pc1.onicecandidate = event => {
      if (event.candidate == null) return;
      pc2.addIceCandidate(new RTCIceCandidate(event.candidate));
    };
    pc2 = new RTCPeerConnection(null);
    pc2.onaddstream = event => {
      theStreamB = event.stream;
      addStreamToVideoTag(event.stream, 'remoteVideo');
    };
    pc2.onicecandidate = event => {
      if (event.candidate == null) return;
      pc1.addIceCandidate(new RTCIceCandidate(event.candidate));
    };

    pc1.createOffer({offerToReceiveVideo: 1})
      .then(desc => {
        pc1.setLocalDescription(desc);
        pc2.setRemoteDescription(desc);
        return pc2.createAnswer({offerToReceiveVideo: 1});
      })
      .then(desc => {
        pc1.setRemoteDescription(desc);
        pc2.setLocalDescription(desc);
      })
      .catch(err => {
        console.error('createOffer()/createAnswer() failed' + err);
      });
  }, function (err) {
    alert('Error:' + err);
  });
}

function addStreamToVideoTag(stream, tag) {
  var mediaControl = document.getElementById(tag);
  if ('srcObject' in mediaControl) {
    mediaControl.srcObject = stream;
  } else if (navigator.mozGetUserMedia) {
    mediaControl.mozSrcObject = stream;
  } else {
    mediaControl.src = (window.URL || window.webkitURL).createObjectURL(stream);
  }
}

Shape Detection API

Shape Detection API 是一组向 Web 应用程序公开底层系统的图像处理(如 OCR(文本检测)、条形码 / QR 扫描或人脸检测功能)的服务。 检测的可用性和质量因操作系统和硬件而异,API 按原样公开这些服务。

function writeLog(message) {
  const newState = document.createElement('p');
  newState.innerHTML = message;
  document.getElementById('target').appendChild(newState);
}

function detectText() {
  if (!('TextDetector' in window)) {
    alert('TextDetector is not available');
    return;
  }
  const file = document.getElementById('file').files[0]
  if (!file) {
    alert('No image - upload a file first.');
    return;
  }

  document.getElementById('target').innerHTML = '';
  const detector = new TextDetector();
  createImageBitmap(file)
    .then((image) => detector.detect(image))
    .then((results) => {
      if (results.length) {
        results.forEach((result) => {
          writeLog(`Detected text "${result.rawValue}" at (${Math.round(result.boundingBox.x)},${Math.round(result.boundingBox.y)})`);
        })
      } else {
        writeLog('No texts detected.');
      }
    })
    .catch((err) => writeLog('Text detection error:' + err));
}

function detectBarcode() {
  if (!('BarcodeDetector' in window)) {
    alert('BarcodeDetector is not available');
    return;
  }
  const file = document.getElementById('file').files[0]
  if (!file) {
    alert('No image - upload a file first.');
    return;
  }

  document.getElementById('target').innerHTML = '';
  const detector = new BarcodeDetector();

  createImageBitmap(file)
    .then((image) => detector.detect(image))
    .then((results) => {
      if (results.length) {
        results.forEach((result) => {
          writeLog(`Detected text "${result.rawValue}" at (${Math.round(result.boundingBox.x)},${Math.round(result.boundingBox.y)})`);
        })
      } else {
        writeLog('No barcodes detected.');
      }
    })
    .catch((err) => writeLog('Barcode detection error:' + err));
}

function detectFace() {
  if (!('FaceDetector' in window)) {
    alert('FaceDetector is not available');
    return;
  }

  const file = document.getElementById('file').files[0]
  if (!file) {
    alert('No image - upload a file first.');
    return;
  }

  document.getElementById('target').innerHTML = '';
  const detector = new FaceDetector();

  createImageBitmap(file)
    .then((image) => detector.detect(image))
    .then((results) => {
      if (results.length) {
        results.forEach((result) => {
          writeLog(`Detected face with ${result.landmarks.map((l) => l.type).join()} at (${Math.round(result.boundingBox.x)},${Math.round(result.boundingBox.y)})`);
        })
      } else {
        writeLog('No faces detected.');
      }
    })
    .catch((err) => writeLog('Face detection error:' + err));
}

6.PWA 与 Device Features

Network Type & Speed

Network Information API 允许 Web 应用程序读取当前网络类型以及基于客户端使用的底层连接技术假定的最大下行链路速度,同时还允许在网络类型发生更改时订阅通知。

function getConnection() {
  return navigator.connection || navigator.mozConnection ||
    navigator.webkitConnection || navigator.msConnection;
}
function updateNetworkInfo(info) {
  document.getElementById('networkType').innerHTML = info.type;
  document.getElementById('effectiveNetworkType').innerHTML = info.effectiveType;
  document.getElementById('downlinkMax').innerHTML = info.downlinkMax;
}
var info = getConnection();
if (info) {
  info.onchange = function (event) {
    updateNetworkInfo(event.target);
  }
  updateNetworkInfo(info);
}

Online State

浏览器向 Web 应用程序公开网络连接可用性信息,以便应用程序可以做出正确反应,即在检测到离线情况时停止所有利用网络的操作并切换到缓存数据。

navigator.onLine
window.addEventListener('online', listener)
window.addEventListener('offline', listener)

Vibration

Vibration API 允许 Web 应用程序使用设备的内置振动(如果存在)。

function vibrateSimple() {
  navigator.vibrate(200);
}
function vibratePattern() {
  navigator.vibrate([100, 200, 200, 200, 500]);
}

Battery Status API

Battery Status API 允许 Web 应用程序获取有关设备电源、电池电量、预期充电或放电时间的信息。 每当任何可用信息发生变化时,它还会公开事件。 API 允许应用程序根据功率级别打开或者关闭其低能效操作。

if ('getBattery' in navigator || ('battery' in navigator && 'Promise' in window)) {
  var target = document.getElementById('target');
  function handleChange(change) {
    var timeBadge = new Date().toTimeString().split(' ')[0];
    var newState = document.createElement('p');
    newState.innerHTML = ''+ timeBadge +' '+ change +'.';
    target.appendChild(newState);
  }

  function onChargingChange() {
    handleChange('Battery charging changed to' + (this.charging ? 'charging' : 'discharging') + '')
  }
  function onChargingTimeChange() {
    handleChange('Battery charging time changed to' + this.chargingTime + 's');
  }
  function onDischargingTimeChange() {
    handleChange('Battery discharging time changed to' + this.dischargingTime + 's');
  }
  function onLevelChange() {
    handleChange('Battery level changed to' + this.level + '');
  }

  var batteryPromise;

  if ('getBattery' in navigator) {
    batteryPromise = navigator.getBattery();
  } else {
    batteryPromise = Promise.resolve(navigator.battery);
  }
  batteryPromise.then(function (battery) {
    document.getElementById('charging').innerHTML = battery.charging ? 'charging' : 'discharging';
    document.getElementById('chargingTime').innerHTML = battery.chargingTime + 's';
    document.getElementById('dischargingTime').innerHTML = battery.dischargingTime + 's';
    document.getElementById('level').innerHTML = battery.level;

    battery.addEventListener('chargingchange', onChargingChange);
    battery.addEventListener('chargingtimechange', onChargingTimeChange);
    battery.addEventListener('dischargingtimechange', onDischargingTimeChange);
    battery.addEventListener('levelchange', onLevelChange);
  });
}

Device Memory

Device Memory API 允许 Web 应用程序根据安装的 RAM 内存的大小来评估设备的类别。 出于性能原因,它可用于识别低端设备以提供精简、轻量级的网站体验。 API 提供的值并不暗示有多少内存实际可供应用程序使用 ,其目的仅用作设备类别指示。

document.getElementById('result').innerHTML = navigator.deviceMemory || 'unknown'

7.PWA 与 Operating System(操作系统)

Offline Storage

Web 应用程序的离线存储功能的原型和标准化技术进行了多次迭代。 第一次尝试要么只是一些简单的解决方法(例如:将数据存储在 cookie 中),要么需要额外的软件(例如: Flash 或 Google Gears)。 后来,Web SQL 的想法(基本上是在浏览器中原生包含 SQLite)被创造并在某些浏览器中实现,但后来由于标准化困难而被弃用。

目前至少有三种不同且独立的技术已标准化并可用。 最简单的是 Web Storage ,一种键值字符串存储,允许 Web 应用程序持久地跨窗口存储数据 (localStorage) 或在单个浏览器选项卡中存储单个会话的数据 (sessionStorage)。 更复杂的 IndexedDB 是一个基于类似数据库结构的底层 API,其中事务和游标通过索引进行迭代。 而 最新的 Cache API 是一个专门的解决方案,用于保存请求 、 响应对,主要在 Service Worker 实现中有用。

任何持久性存储(无论是 localStorage、IndexedDB 还是 Cache API)中存储的数据的实际持久性都是由浏览器管理的,默认在内存压力情况下,可能会在未经最终用户同意的情况下被删除。 为了解决这个问题,引入了 Storage API , 它为 Web 应用程序提供了一种在用户允许的情况下以完全可靠的方式存储数据的方法。

if ('localStorage' in window || 'sessionStorage' in window) {
  var selectedEngine;

  var logTarget = document.getElementById('target');
  var valueInput = document.getElementById('value');

  var reloadInputValue = function () {
  console.log(selectedEngine, window[selectedEngine].getItem('myKey'))
    valueInput.value = window[selectedEngine].getItem('myKey') || '';
  }

  var selectEngine = function (engine) {
    selectedEngine = engine;
    reloadInputValue();
  };

  function handleChange(change) {
    var timeBadge = new Date().toTimeString().split(' ')[0];
    var newState = document.createElement('p');
    newState.innerHTML = ''+ timeBadge +' '+ change +'.';
    logTarget.appendChild(newState);
  }

  var radios = document.querySelectorAll('#selectEngine input');
  for (var i = 0; i < radios.length; ++i) {
    radios[i].addEventListener('change', function () {
      selectEngine(this.value)
    });
  }
  selectEngine('localStorage');
  valueInput.addEventListener('keyup', function () {
    window[selectedEngine].setItem('myKey', this.value);
  });

  var onStorageChanged = function (change) {
    var engine = change.storageArea === window.localStorage ? 'localStorage' : 'sessionStorage';
    handleChange('External change in' + engine + ': key' + change.key + 'changed from' + change.oldValue + 'to' + change.newValue + '');
    if (engine === selectedEngine) {
      reloadInputValue();
    }
  }

  window.addEventListener('storage', onStorageChanged);
}

File Access

File Access API 使 Web 应用程序能够访问有关用户决定与应用程序共享的文件的文件系统级只读信息,即大小、MIME 类型、修改日期、内容,而无需将文件发送到服务器。

function getReadFile(reader, i) {
  return function () {
    var li = document.querySelector('[data-idx="' + i + '"]');

    li.innerHTML += 'File starts with"' + reader.result.substr(0, 25) + '"';
  }
}

function readFiles(files) {
  document.getElementById('count').innerHTML = files.length;

  var target = document.getElementById('target');
  target.innerHTML = '';

  for (var i = 0; i < files.length; ++i) {
    var item = document.createElement('li');
    item.setAttribute('data-idx', i);
    var file = files[i];

    var reader = new FileReader();
    reader.addEventListener('load', getReadFile(reader, i));
    reader.readAsText(file);

    item.innerHTML = ''+ file.name +', '+ file.type +', '+ file.size +' bytes, last modified '+ file.lastModifiedDate +'';
    target.appendChild(item);
  };
}
async function writeFile() {
  if (!window.chooseFileSystemEntries) {
    alert('Native File System API not supported');
    return;
  }
  const target = document.getElementById('target');
  target.innerHTML = 'Opening file handle...';

  const handle = await window.chooseFileSystemEntries({
    type: 'save-file',
  });

  const file = await handle.getFile()
  const writer = await handle.createWriter();
  await writer.write(0, 'Hello world from What Web Can Do!');
  await writer.close()

  target.innerHTML = 'Test content written to' + file.name + '.';
}
HTML
<div class="columns">
  <div class="column">
    <button class="btn-file">
      Choose some files to read<br>(File API) <input type="file" onchange="readFiles(this.files)" multiple>
    </button>

    <p>Number of selected files: <b id="count">N/A</b></p>
  </div>
  <div class="column">
    <button class="btn-file" onclick="writeFile()">
      Choose file to create or overwrite<br>(Native File System API)
    </button>
  </div>
</div>

<ul id="target"></ul>
CSS
.btn-file {
    position: relative;
    overflow: hidden;
    margin: 10px;
}
.btn-file input[type=file] {
    position: absolute;
    top: 0;
    right: 0;
    min-width: 100%;
    min-height: 100%;
    opacity: 0;
    outline: none;
    background: #fff;
    cursor: inherit;
    display: block;
}

Storage Quotas

Storage Quotas 用于通过 Google Chrome 进行的,以允许 Web 应用程序查询系统当前使用的和可供应用程序使用的存储空间的大小。

最新的 Quota Estimation API 还包括一种请求浏览器保留所存储数据的方法,否则这些数据将在系统发出内存压力信号时被清除, 请求此持久存储功能的权限可能由浏览器基于启发式授予(即 Google Chrome),或者可能需要明确的用户同意(即 Firefox)。

旧的实现仅在带有 webkit- 前缀的 Chrome 中受支持,用于保持临时存储和持久存储之间的分离,并允许 Web 应用程序在需要时请求更多存储空间。

if ('storage' in navigator && 'estimate' in navigator.storage) {
  navigator.storage.estimate()
    .then(estimate => {
      document.getElementById('usage').innerHTML = estimate.usage;
      document.getElementById('quota').innerHTML = estimate.quota;
      document.getElementById('percent').innerHTML = (estimate.usage * 100 / estimate.quota).toFixed(0);
    });
}

if ('storage' in navigator && 'persisted' in navigator.storage) {
  navigator.storage.persisted()
    .then(persisted => {
      document.getElementById('persisted').innerHTML = persisted ? 'persisted' : 'not persisted';
    });
}

function requestPersistence() {
  if ('storage' in navigator && 'persist' in navigator.storage) {
    navigator.storage.persist()
      .then(persisted => {
        document.getElementById('persisted').innerHTML = persisted ? 'persisted' : 'not persisted';
      });
  }
}

8.PWA 与用户 Input 输入

Touch Gestures

传统意义上,Web 依赖鼠标和键盘作为唯一的输入设备,而移动设备主要通过触摸控制。 移动 Web 从一个有点棘手的问题开始,即将触摸事件转换为鼠标事件(例如 mousedown)。

较新的 HTML5 方法是将 touch 作为一流的输入方式,允许 Web 应用程序拦截和识别复杂的多点触摸手势、徒手绘图等。不幸的是,目前要么通过触摸事件,例如 touchstart,这是供应商走的路线,或者通过由微软发起的更新、更通用的指针事件规范时,苹果公司后来将其标准化为事实上的解决方案。

function startDrag(e) {
  this.ontouchmove = this.onmspointermove = moveDrag;

  this.ontouchend = this.onmspointerup = function () {
    this.ontouchmove = this.onmspointermove = null;
    this.ontouchend = this.onmspointerup = null;
  }

  var pos = [this.offsetLeft, this.offsetTop];
  var that = this;
  var origin = getCoors(e);

  function moveDrag(e) {
    var currentPos = getCoors(e);
    var deltaX = currentPos[0] - origin[0];
    var deltaY = currentPos[1] - origin[1];
    this.style.left = (pos[0] + deltaX) + 'px';
    this.style.top = (pos[1] + deltaY) + 'px';
    return false; // cancels scrolling
  }

  function getCoors(e) {
    var coors = [];
    if (e.targetTouches && e.targetTouches.length) {
      var thisTouch = e.targetTouches[0];
      coors[0] = thisTouch.clientX;
      coors[1] = thisTouch.clientY;
    } else {
      coors[0] = e.clientX;
      coors[1] = e.clientY;
    }
    return coors;
  }
}

var elements = document.querySelectorAll('.test-element');
[].forEach.call(elements, function (element) {
  element.ontouchstart = element.onmspointerdown = startDrag;
});

document.ongesturechange = function () {
  return false;
}

Speech Recognition

Web Speech API 的语音识别部分允许授权的 Web 应用程序访问设备的麦克风并生成所录制语音的文字记录,从而使得 Web 应用程序可以使用语音作为输入和控制方法之一,类似于触摸或键盘。

从技术上讲,语音识别功能也可以通过访问麦克风并使用 Web Audio API 处理音频流来实现,采用这种方法的典型示例库是 pocketsphinx.js。

let recognition = new SpeechRecognition()

Clipboard (Copy & Paste)

Clipboard API 为 Web 应用程序提供了一种对用户执行的剪切、复制和粘贴操作做出反应以及代表用户直接读取或写入系统剪贴板的方法。

有两种类型的剪贴板 API 可用 ,比如:较旧的同步式和较新的异步式。 较新的 API 仅限于 HTTPS,并且需要明确的用户权限才能进行粘贴操作 ,但截至 2020 年初在 Safari 中依然不可用。 旧的 API 没有正确解决隐私问题,因此粘贴功能在大多数浏览器中不再起作用。

var logTarget = document.getElementById('logTarget');

function useAsyncApi() {
  return document.querySelector('input[value=async]').checked;
}

function log(event) {
  var timeBadge = new Date().toTimeString().split(' ')[0];
  var newInfo = document.createElement('p');
  newInfo.innerHTML = ''+ timeBadge +' '+ event +'.';
  logTarget.appendChild(newInfo);
}

function performCopyEmail() {
  var selection = window.getSelection();
  var emailLink = document.querySelector('.js-emaillink');

  if (useAsyncApi()) {
    // 剪切板
    navigator.clipboard.writeText(emailLink.textContent)
      .then(() => log('Async writeText successful,"' + emailLink.textContent + '"written'))
      .catch(err => log('Async writeText failed with error:"' + err + '"'));
  } else {
    selection.removeAllRanges();
    var range = document.createRange();
    range.selectNode(emailLink);
    selection.addRange(range);

    try {
      var successful = document.execCommand('copy');
      var msg = successful ? 'successful' : 'unsuccessful';
      log('Copy email command was' + msg);
    } catch (err) {
      log('execCommand Error', err);
    }

    selection.removeAllRanges();
  }
}

function performCutTextarea() {
  var cutTextarea = document.querySelector('.js-cuttextarea');

  if (useAsyncApi()) {
    navigator.clipboard.writeText(cutTextarea.textContent)
      .then(() => {
        log('Async writeText successful,"' + cutTextarea.textContent + '"written');
        cutTextarea.textContent = '';
      })
      .catch(err => log('Async writeText failed with error:"' + err + '"'));
  } else {
    var hasSelection = document.queryCommandEnabled('cut');
    cutTextarea.select();

    try {
      var successful = document.execCommand('cut');
      var msg = successful ? 'successful' : 'unsuccessful';
      log('Cutting text command was' + msg);
    } catch (err) {
      log('execCommand Error', err);
    }
  }
}

function performPaste() {
  var pasteTextarea = document.querySelector('.js-cuttextarea');

  if (useAsyncApi()) {
    navigator.clipboard.readText()
      .then((text) => {
        pasteTextarea.textContent = text;
        log('Async readText successful,"' + text + '"written');
      })
      .catch((err) => log('Async readText failed with error:"' + err + '"'));
  } else {
    pasteTextarea.focus();
    try {
      var successful = document.execCommand('paste');
      var msg = successful ? 'successful' : 'unsuccessful';
      log('Pasting text command was' + msg);
    } catch (err) {
      log('execCommand Error', err);
    }
  }
}

// Get the buttons
var cutTextareaBtn = document.querySelector('.js-textareacutbtn');
var copyEmailBtn = document.querySelector('.js-emailcopybtn');
var pasteTextareaBtn = document.querySelector('.js-textareapastebtn');

// Add click event listeners
copyEmailBtn.addEventListener('click', performCopyEmail);
cutTextareaBtn.addEventListener('click', performCutTextarea);
pasteTextareaBtn.addEventListener('click', performPaste);

function logUserOperation(event) {
  log('User performed' + event.type + 'operation. Payload is:' + event.clipboardData.getData('text/plain') + '');
}

document.addEventListener('cut', logUserOperation);
document.addEventListener('copy', logUserOperation);

Pointing Device Adaptation

CSS4 规范的交互媒体部分定义了媒体查询,允许 Web 应用程序根据用户与应用程序交互的方式更改其布局和用户界面。 其允许识别浏览器的主指针(即鼠标、触摸、键盘),并决定它是细还是粗,以及是否可以使用 “经典” 界面(如平板电脑上的触摸)将鼠标悬停在元素上,以便界面可以缩小或放大,并启用悬停交互或相应地用替代方案替换。

@media (hover: hover) {
  #tooltip {
    display: none;
  }
  #button:hover ~ #tooltip {
    display: block;
  }
}

@media (pointer: fine) {
  #button {
    font-size: x-small;
  }
}
@media (pointer: coarse) {
  #button {
    font-size: x-large;
  }
}

Eye Dropper

EyeDropper API 允许用户使用吸管工具从屏幕上捕获样本颜色。

与基于 Chromium 的桌面浏览器上的 <input type="color"> 不同,此 API 提供了一个简单的界面,可以使用标准 API 选择整个设备屏幕的颜色。

// Create an EyeDropper object
let eyeDropper = new EyeDropper();

// Enter eyedropper mode
let icon = document.getElementById("eyeDropperIcon")
let color = document.getElementById("colorCode")
// You may use the dropper only on the cat!
icon.addEventListener('click', e => {
    eyeDropper.open()
    .then(colorSelectionResult => {
        // returns hex color value (#RRGGBB) of the selected pixel
        color.innerText = colorSelectionResult.sRGBHex;
    })
    .catch(error => {
        // handle the user choosing to exit eyedropper mode without a selection
    });
});

下面是 HTML 内容:

<div class="column">
  <p>Click on the image below to activate the dropper</p>
  <img id="eyeDropperIcon" src="/images/cat.jpg"/>
  <p>The hex color of the selected pixel is <b><span id="colorCode">???</span></b></p>
</div>

9.PWA 与 Location & Position

Geolocation

Geolocation API 允许授权的 Web 应用程序访问设备提供的位置数据,其本身是使用 GPS 或从网络环境获得。 除了一次性位置查询之外,还为应用程序提供了一种通知位置更改的方式。

var target = document.getElementById('target');
var watchId;

function appendLocation(location, verb) {
  verb = verb || 'updated';
  var newLocation = document.createElement('p');
  newLocation.innerHTML = 'Location' + verb + ':' + location.coords.latitude + ',' + location.coords.longitude + '';
  target.appendChild(newLocation);
}

if ('geolocation' in navigator) {
  document.getElementById('askButton').addEventListener('click', function () {
    // 获取当前位置
    navigator.geolocation.getCurrentPosition(function (location) {
      appendLocation(location, 'fetched');
    });
    // 更新位置
    watchId = navigator.geolocation.watchPosition(appendLocation);
  });
} else {
  target.innerText = 'Geolocation API not supported.';
}

Device Position

第一代设备位置支持是 Device Orientation API 的一部分,其允许 Web 应用程序访问陀螺仪和指南针数据,以确定用户设备在所有三个维度上的静态方向。

基于 Generic Sensor API 的新规范也存在方向传感器 API(绝对和相对变体)。 与之前的规范相反,它提供了以四元数表示的读数,这使得它直接与 WebGL 等绘图环境兼容。

if ('DeviceOrientationEvent' in window) {
  window.addEventListener('deviceorientation', deviceOrientationHandler, false);
} else {
  document.getElementById('logoContainer').innerText = 'Device Orientation API not supported.';
}

function deviceOrientationHandler (eventData) {
  var tiltLR = eventData.gamma;
  var tiltFB = eventData.beta;
  var dir = eventData.alpha;

  document.getElementById("doTiltLR").innerHTML = Math.round(tiltLR);
  document.getElementById("doTiltFB").innerHTML = Math.round(tiltFB);
  document.getElementById("doDirection").innerHTML = Math.round(dir);

  var logo = document.getElementById("imgLogo");
  logo.style.webkitTransform = "rotate(" + tiltLR + "deg) rotate3d(1,0,0," + (tiltFB * -1) + "deg)";
  logo.style.MozTransform = "rotate(" + tiltLR + "deg)";
  logo.style.transform = "rotate(" + tiltLR + "deg) rotate3d(1,0,0," + (tiltFB * -1) + "deg)";
}

Device Motion

第一代设备运动支持是 Device Orientation API 的一部分,其允许 Web 应用程序访问以加速度(以 m/s2 为单位)表示的加速度计数据和以事件形式提供的三个维度中每个维度的以旋转角度变化(以 °/s 为单位)表示的陀螺仪数据。

自 2018 年中期以来,针对每种传感器类型推出了基于通用传感器 API 的更新的单独规范。 这些 API 可直接访问物理设备(加速计 API、陀螺仪 API 和磁力计 API)的读数以及通过组合物理传感器(线性加速传感器 API 和重力传感器 API)的读数组成的高级融合传感器。

if ('LinearAccelerationSensor' in window && 'Gyroscope' in window) {
  document.getElementById('moApi').innerHTML = 'Generic Sensor API';

  let lastReadingTimestamp;
  let accelerometer = new LinearAccelerationSensor();
  accelerometer.addEventListener('reading', e => {
    if (lastReadingTimestamp) {
      intervalHandler(Math.round(accelerometer.timestamp - lastReadingTimestamp));
    }
    lastReadingTimestamp = accelerometer.timestamp
    accelerationHandler(accelerometer, 'moAccel');
  });
  accelerometer.start();

  if ('GravitySensor' in window) {
    let gravity = new GravitySensor();
    gravity.addEventListener('reading', e => accelerationHandler(gravity, 'moAccelGrav'));
    gravity.start();
  }

  let gyroscope = new Gyroscope();
  gyroscope.addEventListener('reading', e => rotationHandler({
    alpha: gyroscope.x,
    beta: gyroscope.y,
    gamma: gyroscope.z
  }));
  gyroscope.start();

} else if ('DeviceMotionEvent' in window) {
  document.getElementById('moApi').innerHTML = 'Device Motion API';

  var onDeviceMotion = function (eventData) {
    accelerationHandler(eventData.acceleration, 'moAccel');
    accelerationHandler(eventData.accelerationIncludingGravity, 'moAccelGrav');
    rotationHandler(eventData.rotationRate);
    intervalHandler(eventData.interval);
  }

  window.addEventListener('devicemotion', onDeviceMotion, false);
} else {
  document.getElementById('moApi').innerHTML = 'No Accelerometer & Gyroscope API available';
}

function accelerationHandler(acceleration, targetId) {
  var info, xyz = "[X, Y, Z]";

  info = xyz.replace("X", acceleration.x && acceleration.x.toFixed(3));
  info = info.replace("Y", acceleration.y && acceleration.y.toFixed(3));
  info = info.replace("Z", acceleration.z && acceleration.z.toFixed(3));
  document.getElementById(targetId).innerHTML = info;
}

function rotationHandler(rotation) {
  var info, xyz = "[X, Y, Z]";

  info = xyz.replace("X", rotation.alpha && rotation.alpha.toFixed(3));
  info = info.replace("Y", rotation.beta && rotation.beta.toFixed(3));
  info = info.replace("Z", rotation.gamma && rotation.gamma.toFixed(3));
  document.getElementById("moRotation").innerHTML = info;
}

function intervalHandler(interval) {
  document.getElementById("moInterval").innerHTML = interval;
}

10.Screen & Output

Virtual & Augmented Reality

截至 2020 年初,对 Web 应用程序的虚拟和增强现实的支持有限且不一致,有两个可用的 API, 较旧的 WebVR API 可在某些浏览器中用于某些特定的 VR 环境,而较新的 WebXR 设备 API 试图以更通用的方式处理该主题,包括 AR 或混合现实设备,从 2019 年底开始将部署在基于 Chromium 的浏览器中。

两个 API 共享相同的基本概念,范围是允许授权的 Web 应用程序发现可用的 VR/AR 设备,与设备建立会话,读取准备正确渲染所需的特定于设备的几何数据,并将 <canvas> 元素作为可视层绑定到设备上 。

通过这种方式,渲染细节由现有的画布接口(如 WebGL 上下文)处理,并且实现者通常将渲染本身委托给专门的库(如 A-Frame)。

document.getElementById('startVRButton').addEventListener('click', function () {
  if (navigator.xr) {
    checkForXR();
  } else if (navigator.getVRDisplays) {
    checkForVR();
  } else {
    alert('WebXR/WebVR APIs are not supported.');
  }
});

async function checkForXR() {
    if (!await navigator.xr.isSessionSupported('immersive-vr')) {
        alert('No immersive VR device detected');
        return;
    }

    const session = await navigator.xr.requestSession('immersive-vr');

    if (!session.inputSources.length) {
      throw 'VR supported, but no VR input sources available';
    }

    const result = document.getElementById('result');
    result.innerHTML = session.inputSources.length + 'input sources detected';
}

async function checkForVR() {
  try {
    const displays = await navigator.getVRDisplays()

    if (!displays.length) {
      throw 'VR supported, but no VR displays available';
    }

    const result = document.getElementById('result');

    displays.forEach(function (display) {
      let li = document.createElement('li');
      li.innerHTML = display.displayName + '(' + display.displayId + ')';
      result.appendChild(li);
    })

  } catch (err) {
    alert(err);
  }
}

Fullscreen

Fullscreen API 允许 Web 应用程序以全屏模式显示自身或自身的一部分,而浏览器 UI 元素不可见,也是方向锁定的先决条件状态。

var $ = document.querySelector.bind(document);
var $ = function (selector) {
  return [].slice.call(document.querySelectorAll(selector), 0);
}
var target = $('#logTarget');

function logChange (event) {
  var timeBadge = new Date().toTimeString().split(' ')[0];
  var newState = document.createElement('p');
  newState.innerHTML = ''+ timeBadge +' '+ event +'.';
  target.appendChild(newState);
}

Screen Orientation & Lock

Screen Orientation API 允许 Web 应用程序获取有关文档当前方向(纵向或横向)的信息,以及将屏幕方向锁定在请求的状态。

当前版本的规范在 window.screen.orientation 对象中完全定义了此功能。 以前的版本在 Microsoft Edge 中实现过一次,将方向锁定分离为 window.screen.lockOrientation。

var $ = document.getElementById.bind(document);

var orientKey = 'orientation';
if ('mozOrientation' in screen) {
  orientKey = 'mozOrientation';
} else if ('msOrientation' in screen) {
  orientKey = 'msOrientation';
}

var target = $('logTarget');
var device = $('device');
var orientationTypeLabel = $('orientationType');

function logChange (event) {
  var timeBadge = new Date().toTimeString().split(' ')[0];
  var newState = document.createElement('p');
  newState.innerHTML = ''+ timeBadge +' '+ event +'.';
  target.appendChild(newState);
}

if (screen[orientKey]) {
  function update() {
    var type = screen[orientKey].type || screen[orientKey];
    orientationTypeLabel.innerHTML = type;

    var landscape = type.indexOf('landscape') !== -1;

    if (landscape) {
      device.style.width = '180px';
      device.style.height = '100px';
    } else {
      device.style.width = '100px';
      device.style.height = '180px';
    }

    var rotate = type.indexOf('secondary') === -1 ? 0 : 180;
    var rotateStr = 'rotate(' + rotate + 'deg)';

    device.style.webkitTransform = rotateStr;
    device.style.MozTransform = rotateStr;
    device.style.transform = rotateStr;
  }

  update();

  var onOrientationChange = null;

  if ('onchange' in screen[orientKey]) { // newer API
    onOrientationChange = function () {
      logChange('Orientation changed to' + screen[orientKey].type + '');
      update();
    };

    screen[orientKey].addEventListener('change', onOrientationChange);
  } else if ('onorientationchange' in screen) { // older API
    onOrientationChange = function () {
      logChange('Orientation changed to' + screen[orientKey] + '');
      update();
    };

    screen.addEventListener('orientationchange', onOrientationChange);
  }

  // browsers require full screen mode in order to obtain the orientation lock
  var goFullScreen = null;
  var exitFullScreen = null;
  if ('requestFullscreen' in document.documentElement) {
    goFullScreen = 'requestFullscreen';
    exitFullScreen = 'exitFullscreen';
  } else if ('mozRequestFullScreen' in document.documentElement) {
    goFullScreen = 'mozRequestFullScreen';
    exitFullScreen = 'mozCancelFullScreen';
  } else if ('webkitRequestFullscreen' in document.documentElement) {
    goFullScreen = 'webkitRequestFullscreen';
    exitFullScreen = 'webkitExitFullscreen';
  } else if ('msRequestFullscreen') {
    goFullScreen = 'msRequestFullscreen';
    exitFullScreen = 'msExitFullscreen';
  }

Wake Lock

只要应用程序持有该资源的锁,Wake Lock API 就允许 Web 应用程序防止屏幕或系统等资源变得不可用。 API 的目的是让用户或应用程序不间断地完成正在进行的长时间活动(例如导航或阅读)。

在某些浏览器中实验性的初始实现尝试只是一个可由应用程序控制的布尔标志,被认为过于公开而容易被滥用,而且过于含蓄。

自 2019 年中期起,提出了更明确的方法,并可以在 “实验性 Web 平台功能” 标志后面以及通过 Google Chrome 中的 Origin Trial 来使用。 它允许指定请求锁定的资源,尽管目前只有屏幕选项可用。 当外部因素中断锁定时,API 还允许订阅事件。

function printStatus(status) {
  document.getElementById("status").innerHTML = status;
}

let wakeLockObj = null;

function toggle() {
  if ("keepAwake" in screen) {
    screen.keepAwake = !screen.keepAwake;
    printStatus(screen.keepAwake ? 'acquired' : 'not acquired');
  } else if ("wakeLock" in navigator) {
    if (wakeLockObj) {
      wakeLockObj.release();
      wakeLockObj = null;
      printStatus('released');
    } else {
      printStatus('acquiring...');
      navigator.wakeLock.request('screen')
        .then((wakeLock) => {
          wakeLockObj = wakeLock;

          wakeLockObj.addEventListener('release', () => {
            printStatus('released externally');
            wakeLockObj = null;
          })

          printStatus('acquired');
        })
        .catch((err) => {
          console.error(err);
          printStatus('failed to acquire:' + err.message);
        })
    }
  }
}

if ("keepAwake" in screen) {
  document.getElementById("api").innerHTML = 'screen.keepAwake';
  printStatus('not acquired');
} else if ("wakeLock" in navigator) {
  document.getElementById("api").innerHTML = 'navigator.wakeLock';
  printStatus('not acquired');
}

Presentation Features

Presentation API 的目的是让 Web 应用程序可以使用演示显示模式,用于呈现的显示器可以与浏览器正在使用的显示器相同,但也可以是外部显示设备。 浏览器可以充当演示的发起者以及接收在演示显示器上外部发起的到演示的连接。

目前该 API 仅在 Chrome 和 Opera、桌面版和 Android 上受支持。

navigator.presentation.defaultRequest = new PresentationRequest(presentationUrl)
request.getAvailability()
availability.addEventListener('change', listener)

参考资料

https://whatwebcando.today/offline.html

https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API/Using_the_Payment_Request_API

https://whatwebcando.today/

https://www.emclient.com/blog/em-client-features--offline-mode-157

最近发表
标签列表