整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

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

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

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

文章从用户体验、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

者:当耐特

来 源:cnblogs.com/iamzhanglei/p/6177961.html

广而告之:由于此订阅号换了个皮肤,系统自动取消了读者的公众号置顶。导致用户接受文章不及时。可以打开订阅号,选择置顶(标星)公众号,重磅干货,第一时间送达!

一秒钟把Github项目变成前端网站

GitHub Pages大家可能都知道,常用的做法,是建立一个gh-pages的分支,通过setting里的设置的GitHub Pages模块可以自动创建该项目的网站。

这里经常遇到的痛点是,master遇到变更,经常需要去sync到gh-pages,特别是纯web前端项目,这样的痛点是非常地痛。

Github官方可能嗅觉到了该痛点,出了个master当作网站是选项,太有用了。

选择完master branch之后,master自动变成了网站。master所有的提交会自动更新到网站。

精准分享关键代码

比如你有一个文件里的某一行代码写得非常酷炫或者关键,想分享一下。

可以在url后面加上#L行号

比如,点击下面这个url:

https://github.com/AlloyTeam/AlloyTouch/blob/master/alloy_touch.js#L240

你便会跳到alloy_touch.js的第240行。

那么问题来了?如果我是一段代码,即多行代码想分享呢?也很简单:url后面加上

#L开始行号-L结束行号

比如,AlloyTouch的运动缓动和逆向缓动函数如下面代码段所示:

https://github.com/AlloyTeam/AlloyTouch/blob/master/alloy_touch.js#L39-L45

其实也不用记忆你直接在网址后面操作,github自动会帮你生成url。比如你点击39行,url变成了

https://github.com/AlloyTeam/AlloyTouch/blob/master/alloy_touch.js#L39

再按住shift点击45行,url变成了

https://github.com/AlloyTeam/AlloyTouch/blob/master/alloy_touch.js#L39-L45

然后你这个url就可以复制分享出去了,点击这个url的人自动会跳到39行,并且39-45行高亮。

通过提交的msg自动关闭issues

比如有人提交了个issues https://github.com/AlloyTeam/AlloyTouch/issues/6然后你去主干上改代码,改完之后提交填msg的时候,填入:

fix https://github.com/AlloyTeam/AlloyTouch/issues/6

这个issues会自动被关闭。当然不仅仅是fix这个关键字。下面这些关键字也可以:

  • close

  • closes

  • closed

  • fixes

  • fixed

  • resolve

  • resolves

  • resolved

通过HTML方式嵌入Github

如下面所示,user和repo改成你想要展示的便可以

<iframe src="//ghbtns.com/github-btn.html?

user=alloyteam&repo=alloytouch&type=watch&count=true"

allowtransparency="true"

frameborder="0" scrolling="0"

width="110" height="20">

</iframe>

插入之后你便可以看到这样的展示:

gitattributes设置项目语言

如上图所示,github会根据相关文件代码的数量来自动识别你这个项目是HTML项目还是Javascript项目。

这就带来了一个问题,比如AlloyTouch最开始被识别成HTML项目。

因为HTML例子比JS文件多。怎么办呢?gitattributes来帮助你搞定。在项目的根目录下添加如下.gitattributes文件便可

https://github.com/AlloyTeam/AlloyTouch/blob/master/.gitattributes

里面的:

*.html linguist-language=JavaScript

主要意思是把所有html文件后缀的代码识别成js文件。

查看自己项目的访问数据

在自己的项目下,点击Graphs,然后再点击Traffic如下所示:

里面有Referring sites和Popular content的详细数据和排名。如:Referring sites

其中Referring sites代表大家都是从什么网站来到你的项目的,Popular content代表大家经常看你项目的哪些文件。

trending排行榜

上面教大家设置语言了,下面可以看看怎么查看某类型语言的每日排行榜。比如js每日排行榜:

https://github.com/trending/javascript?since=daily

https://github.com/trending/html?since=daily

https://github.com/trending/css?since=daily

Github推荐:https://github.com/explore

其他

  • issue中输入冒号 : 添加表情

  • 任意界面,shift + ?显示快捷键

  • issue中选中文字,R键快速引用

最后

好了,我就会这么多,也是我经常使用的技巧。欢迎补充实用的技巧,我会持续更新上去…

如何置顶、标星公众号?

1.Spring Boot 为什么这么火火火火火火?

2.一文读懂 Spring Data Jpa!

3.字节跳动、腾讯后台开发面经分享(2019.5)

4.10 行代码构建 RESTful 风格应用

么是JavaScript

JavaScript是一种基于对象和事件驱动的、并具有安全性能的脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果。通常JavaScript脚本是通过嵌入在HTML中来实现自身的功能的。

JavaScript特点

是一种解释性脚本语言(代码不进行预编译)。

主要用来向HTML(标准通用标记语言下的一个应用)页面添加交互行为。

可以直接嵌入HTML页面,但写成单独的js文件有利于结构和行为的分离

跨平台特性,在绝大多数浏览器的支持下,可以在多种平台下运行(如Windows、Linux、Mac、Android、iOS等)。

JavaScript组成


JavaScript日常用途

1、嵌入动态文本于HTML页面。

2、对浏览器事件做出响应。

3、读写HTML元素

4、在数据被提交到服务器之前验证数据。

5、检测访客的浏览器信息。

6、控制cookies,包括创建和修改等。

7、基于Node.js技术进行服务器端编程。

JavaScript的基本结构

<script type="text/javascript">
 <!—
 JavaScript 语句;
 —>
</script >


示例:

……
<title>初学JavaScript</title>
</head>
<body>
<script type="text/javascript">
 document.write("初学JavaScript");
 document.write("<h1>Hello,JavaScript</h1>");
</script>
</body>
</html>


<script>…</script>可以包含在文档中的任何地方,只要保证这些代码在被使用前已读取并加载到内存即可

JavaScript的执行原理


网页中引用JavaScript的方式

1、使用<script>标签

2、外部JS文件

<script src="export.js" type="text/javascript"></script>


3.直接在HTML标签中

<input name="btn" type="button" value="弹出消息框" 
 onclick="javascript:alert('欢迎你');"/>


JavaScript核心语法:


1. 变量

①先声明变量再赋值

var width;
width = 5;
var - 用于声明变量的关键字
width - 变量名


②同时声明和赋值变量

var catName= "皮皮";
var x, y, z = 10;


③不声明直接赋值【一般不使用】

width=5;


变量可以不经声明而直接使用,但这种方法很容易出错,也很难查找排错,不推荐使用。

2. 数据类型

①undefined:示例:var width;

变量width没有初始值,将被赋予值undefined

②null:表示一个空值,与undefined值相等

③number:

var iNum=23; //整数

var iNum=23.0; //浮点数

④Boolean:true和false 但是JS会把他们解析成1;0

⑤String:一组被引号(单引号或双引号)括起来的文本 var string1="This is a string";

3. typeof运算符

typeof检测变量的返回值;typeof运算符返回值如下:

①undefined:变量被声明后,但未被赋值.

②string:用单引号或双引号来声明的字符串。

③boolean:true或false。

④number:整数或浮点数。

⑤object:javascript中的对象、数组和null。