ruanyf-tutorial-Web API

  目录
  1. 1. Canvas API
    1. 1.1. 概述
    2. 1.2. Canvas API:绘制图形
      1. 1.2.1. 路径
      2. 1.2.2. 线型
      3. 1.2.3. 矩形
      4. 1.2.4. 弧线
      5. 1.2.5. 文本
      6. 1.2.6. 渐变色和图像填充
      7. 1.2.7. 阴影
    3. 1.3. Canvas API:图像处理
      1. 1.3.1. CanvasRenderingContext2D.drawImage()
      2. 1.3.2. 像素读写
      3. 1.3.3. CanvasRenderingContext2D.save(),CanvasRenderingContext2D.restore()
      4. 1.3.4. CanvasRenderingContext2D.canvas
      5. 1.3.5. 图像变换
    4. 1.4. <canvas> 元素的方法
      1. 1.4.1. HTMLCanvasElement.toDataURL()
      2. 1.4.2. HTMLCanvasElement.toBlob()
    5. 1.5. Canvas 使用实例
      1. 1.5.1. 动画效果
      2. 1.5.2. 像素处理
    6. 1.6. 参考链接
  2. 2. 剪贴板操作 Clipboard API 教程
    1. 2.1. 简介
    2. 2.2. Document.execCommand() 方法
    3. 2.3. 异步 Clipboard API
    4. 2.4. Clipboard 对象
      1. 2.4.1. Clipboard.readText()
      2. 2.4.2. Clipboard.read()
      3. 2.4.3. Clipboard.writeText()
      4. 2.4.4. Clipboard.write()
    5. 2.5. copy 事件,cut 事件
    6. 2.6. paste 事件
    7. 2.7. 参考链接
  3. 3. Fetch API 教程
    1. 3.1. 基本用法
    2. 3.2. Response 对象:处理 HTTP 回应
      1. 3.2.1. Response 对象的同步属性
      2. 3.2.2. 判断请求是否成功
      3. 3.2.3. Response.headers 属性
      4. 3.2.4. 读取内容的方法
      5. 3.2.5. Response.clone()
      6. 3.2.6. Response.body 属性
    3. 3.3. fetch()的第二个参数:定制 HTTP 请求
    4. 3.4. fetch()配置对象的完整 API
    5. 3.5. 取消fetch()请求
    6. 3.6. 参考链接
  4. 4. FontFace API
  5. 5. FormData 对象
    1. 5.1. 简介
    2. 5.2. 实例方法
      1. 5.2.1. append()
      2. 5.2.2. delete()
      3. 5.2.3. entries()
      4. 5.2.4. get()
      5. 5.2.5. getAll()
      6. 5.2.6. has()
      7. 5.2.7. keys()
      8. 5.2.8. set()
      9. 5.2.9. values()
  6. 6. Geolocation API
    1. 6.1. Geolocation 对象
      1. 6.1.1. Geolocation.getCurrentPosition()
      2. 6.1.2. Geolocation.watchPosition()
      3. 6.1.3. Geolocation.clearWatch()
    2. 6.2. Coordinates 对象
    3. 6.3. 参考链接
  7. 7. Headers 对象
    1. 7.1. 简介
    2. 7.2. 构造函数
    3. 7.3. 实例方法
      1. 7.3.1. append()
      2. 7.3.2. delete()
      3. 7.3.3. entries()
      4. 7.3.4. forEach()
      5. 7.3.5. get()
      6. 7.3.6. getSetCookie()
      7. 7.3.7. has()
      8. 7.3.8. keys()
      9. 7.3.9. set()
      10. 7.3.10. values()
  8. 8. IntersectionObserver
    1. 8.1. 简介
    2. 8.2. IntersectionObserver.observe()
      1. 8.2.1. callback 参数
      2. 8.2.2. IntersectionObserverEntry 对象
      3. 8.2.3. Option 对象
    3. 8.3. 实例
      1. 8.3.1. 惰性加载(lazy load)
      2. 8.3.2. 无限滚动
      3. 8.3.3. 视频自动播放
    4. 8.4. 参考链接
  9. 9. Intl.RelativeTimeFormat
    1. 9.1. 基本用法
    2. 9.2. Intl.RelativeTimeFormat.prototype.format()
    3. 9.3. Intl.RelativeTimeFormat.prototype.formatToParts()
    4. 9.4. 参考链接
  10. 10. Intl segmenter API
    1. 10.1. 简介
    2. 10.2. 静态方法
      1. 10.2.1. Intl.Segmenter.supportedLocalesOf()
    3. 10.3. 实例方法
      1. 10.3.1. resolvedOptions()
      2. 10.3.2. segment()
  11. 11. Page Lifecycle API
    1. 11.1. 生命周期阶段
    2. 11.2. 常见场景
    3. 11.3. 事件
      1. 11.3.1. focus 事件
      2. 11.3.2. blur 事件
      3. 11.3.3. visibilitychange 事件
      4. 11.3.4. freeze 事件
      5. 11.3.5. resume 事件
      6. 11.3.6. pageshow 事件
      7. 11.3.7. pagehide 事件
      8. 11.3.8. beforeunload 事件
      9. 11.3.9. unload 事件
    4. 11.4. 获取当前阶段
    5. 11.5. document.wasDiscarded
    6. 11.6. 参考链接
  12. 12. Page Visibility API
    1. 12.1. 简介
    2. 12.2. document.visibilityState
    3. 12.3. document.hidden
    4. 12.4. visibilitychange 事件
    5. 12.5. 页面卸载
    6. 12.6. 参考链接
  13. 13. Request API
    1. 13.1. 构造方法
    2. 13.2. 实例属性
    3. 13.3. 实例方法
      1. 13.3.1. 取出数据体的方法
      2. 13.3.2. clone()
  14. 14. Response API
    1. 14.1. 构造方法
    2. 14.2. 实例属性
      1. 14.2.1. body,bodyUsed
      2. 14.2.2. headers
      3. 14.2.3. ok
      4. 14.2.4. redirected
      5. 14.2.5. status,statusText
      6. 14.2.6. type
      7. 14.2.7. url
    3. 14.3. 实例方法
      1. 14.3.1. 数据读取
      2. 14.3.2. clone()
    4. 14.4. 静态方法
      1. 14.4.1. Response.json()
      2. 14.4.2. Response.error()
      3. 14.4.3. Response.redirect()
  15. 15. Server-Sent Events
    1. 15.1. 简介
    2. 15.2. 与 WebSocket 的比较
    3. 15.3. 客户端 API
      1. 15.3.1. EventSource 对象
      2. 15.3.2. readyState 属性
      3. 15.3.3. url 属性
      4. 15.3.4. withCredentials 属性
      5. 15.3.5. onopen 属性
      6. 15.3.6. onmessage 属性
      7. 15.3.7. onerror 属性
      8. 15.3.8. 自定义事件
      9. 15.3.9. close() 方法
    4. 15.4. 服务器实现
      1. 15.4.1. 数据格式
      2. 15.4.2. data 字段
      3. 15.4.3. id 字段
      4. 15.4.4. event 字段
      5. 15.4.5. retry 字段
    5. 15.5. Node 服务器实例
    6. 15.6. 参考链接
  16. 16. SVG 图像
    1. 16.1. 概述
    2. 16.2. 语法
      1. 16.2.1. <svg>标签
      2. 16.2.2. <circle>标签
      3. 16.2.3. <line>标签
      4. 16.2.4. <polyline>标签
      5. 16.2.5. <rect>标签
      6. 16.2.6. <ellipse>标签
      7. 16.2.7. <polygon>标签
      8. 16.2.8. <path>标签
      9. 16.2.9. <text>标签
      10. 16.2.10. <use>标签
      11. 16.2.11. <g>标签
      12. 16.2.12. <defs>标签
      13. 16.2.13. <pattern>标签
      14. 16.2.14. <image>标签
      15. 16.2.15. <animate>标签
      16. 16.2.16. <animateTransform>标签
    3. 16.3. JavaScript 操作
      1. 16.3.1. DOM 操作
      2. 16.3.2. 获取 SVG DOM
      3. 16.3.3. 读取 SVG 源码
      4. 16.3.4. SVG 图像转为 Canvas 图像
    4. 16.4. 实例:折线图
    5. 16.5. 参考链接
  17. 17. URL 对象
    1. 17.1. 构造函数
    2. 17.2. 实例属性
    3. 17.3. 静态方法
      1. 17.3.1. URL.createObjectURL()
      2. 17.3.2. URL.revokeObjectURL()
      3. 17.3.3. URL.canParse()
      4. 17.3.4. URL.parse()
    4. 17.4. 实例方法
      1. 17.4.1. toString()
  18. 18. URL Pattern API
    1. 18.1. 简介
    2. 18.2. 构造函数 URLPattern()
      1. 18.2.1. 基本用法
      2. 18.2.2. 模式写法
    3. 18.3. 实例属性
    4. 18.4. 实例方法
      1. 18.4.1. exec()
      2. 18.4.2. test()
  19. 19. URLSearchParams 对象
    1. 19.1. 简介
    2. 19.2. 构造方法
    3. 19.3. 实例方法
      1. 19.3.1. append()
      2. 19.3.2. delete()
      3. 19.3.3. get()
      4. 19.3.4. getAll()
      5. 19.3.5. has()
      6. 19.3.6. set()
      7. 19.3.7. sort()
      8. 19.3.8. entries()
      9. 19.3.9. forEach()
      10. 19.3.10. keys()
      11. 19.3.11. values()
      12. 19.3.12. toString()
    4. 19.4. 实例属性
      1. 19.4.1. size
  20. 20. WebSocket
    1. 20.1. 简介
    2. 20.2. WebSocket 握手
    3. 20.3. 客户端的简单示例
    4. 20.4. 客户端 API
      1. 20.4.1. 构造函数 WebSocket
      2. 20.4.2. webSocket.readyState
      3. 20.4.3. webSocket.onopen
      4. 20.4.4. webSocket.onclose
      5. 20.4.5. webSocket.onmessage
      6. 20.4.6. webSocket.send()
      7. 20.4.7. webSocket.bufferedAmount
      8. 20.4.8. webSocket.onerror
    5. 20.5. WebSocket 服务器
    6. 20.6. 参考链接
  21. 21. Web Share API
    1. 21.1. 概述
    2. 21.2. 接口细节
    3. 21.3. 分享文件
    4. 21.4. 参考链接
  22. 22. window.postMessage() 方法
    1. 22.1. 简介
    2. 22.2. 参数和返回值
    3. 22.3. message 事件
    4. 22.4. 实例

Web API学习教程-来自网道项目

Canvas API

概述

<canvas>元素用于生成图像。它本身就像一个画布,JavaScript 通过操作它的 API,在上面生成图像。它的底层是一个个像素,基本上<canvas>是一个可以用 JavaScript 操作的位图(bitmap)。

它与 SVG 图像的区别在于,<canvas>是脚本调用各种方法生成图像,SVG 则是一个 XML 文件,通过各种子元素生成图像。

使用 Canvas API 之前,需要在网页里面新建一个<canvas>元素。

1
2
3
<canvas id="myCanvas" width="400" height="250">
您的浏览器不支持 Canvas
</canvas>

如果浏览器不支持这个 API,就会显示<canvas>标签中间的文字:“您的浏览器不支持 Canvas”。

每个<canvas>元素都有一个对应的CanvasRenderingContext2D对象(上下文对象)。Canvas API 就定义在这个对象上面。

1
2
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

上面代码中,<canvas>元素节点对象的getContext()方法,返回的就是CanvasRenderingContext2D对象。

注意,Canvas API 需要getContext方法指定参数2d,表示该<canvas>节点生成 2D 的平面图像。如果参数是webgl,就表示用于生成 3D 的立体图案,这部分属于 WebGL API。

按照用途,Canvas API 分成两大部分:绘制图形和图像处理。

Canvas API:绘制图形

Canvas 画布提供了一个作图的平面空间,该空间的每个点都有自己的坐标。原点(0, 0)位于图像左上角,x轴的正向是原点向右,y轴的正向是原点向下。

路径

以下方法和属性用来绘制路径。

  • CanvasRenderingContext2D.beginPath():开始绘制路径。
  • CanvasRenderingContext2D.closePath():结束路径,返回到当前路径的起始点,会从当前点到起始点绘制一条直线。如果图形已经封闭,或者只有一个点,那么此方法不会产生任何效果。
  • CanvasRenderingContext2D.moveTo():设置路径的起点,即将一个新路径的起始点移动到(x,y)坐标。
  • CanvasRenderingContext2D.lineTo():使用直线从当前点连接到(x, y)坐标。
  • CanvasRenderingContext2D.fill():在路径内部填充颜色(默认为黑色)。
  • CanvasRenderingContext2D.stroke():路径线条着色(默认为黑色)。
  • CanvasRenderingContext2D.fillStyle:指定路径填充的颜色和样式(默认为黑色)。
  • CanvasRenderingContext2D.strokeStyle:指定路径线条的颜色和样式(默认为黑色)。
1
2
3
4
5
6
7
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.beginPath();
ctx.moveTo(100, 100);
ctx.lineTo(200, 200);
ctx.lineTo(100, 200);

上面代码只是确定了路径的形状,画布上还看不出来,因为没有颜色。所以还需要着色。

1
2
3
ctx.fill()
// 或者
ctx.stroke()

上面代码中,这两个方法都可以使得路径可见。fill()在路径内部填充颜色,使之变成一个实心的图形;stroke()只对路径线条着色。

这两个方法默认都是使用黑色,可以使用fillStylestrokeStyle属性指定其他颜色。

1
2
3
4
5
ctx.fillStyle = 'red';
ctx.fill();
// 或者
ctx.strokeStyle = 'red';
ctx.stroke();

上面代码将填充和线条的颜色指定为红色。

线型

以下的方法和属性控制线条的视觉特征。

  • CanvasRenderingContext2D.lineWidth:指定线条的宽度,默认为1.0。
  • CanvasRenderingContext2D.lineCap:指定线条末端的样式,有三个可能的值:butt(默认值,末端为矩形)、round(末端为圆形)、square(末端为突出的矩形,矩形宽度不变,高度为线条宽度的一半)。
  • CanvasRenderingContext2D.lineJoin:指定线段交点的样式,有三个可能的值:round(交点为扇形)、bevel(交点为三角形底边)、miter(默认值,交点为菱形)。
  • CanvasRenderingContext2D.miterLimit:指定交点菱形的长度,默认为10。该属性只在lineJoin属性的值等于miter时有效。
  • CanvasRenderingContext2D.getLineDash():返回一个数组,表示虚线里面线段和间距的长度。
  • CanvasRenderingContext2D.setLineDash():数组,用于指定虚线里面线段和间距的长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.beginPath();
ctx.moveTo(100, 100);
ctx.lineTo(200, 200);
ctx.lineTo(100, 200);

ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.setLineDash([15, 5]);
ctx.stroke();

上面代码中,线条的宽度为3,线条的末端和交点都改成圆角,并且设置为虚线。

矩形

以下方法用来绘制矩形。

  • CanvasRenderingContext2D.rect():绘制矩形路径。
  • CanvasRenderingContext2D.fillRect():填充一个矩形。
  • CanvasRenderingContext2D.strokeRect():绘制矩形边框。
  • CanvasRenderingContext2D.clearRect():指定矩形区域的像素都变成透明。

上面四个方法的格式都一样,都接受四个参数,分别是矩形左上角的横坐标和纵坐标、矩形的宽和高。

CanvasRenderingContext2D.rect()方法用于绘制矩形路径。

1
2
3
4
5
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.rect(10, 10, 100, 100);
ctx.fill();

上面代码绘制一个正方形,左上角坐标为(10, 10),宽和高都为100。

CanvasRenderingContext2D.fillRect()用来向一个矩形区域填充颜色。

1
2
3
4
5
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 100, 100);

上面代码绘制一个绿色的正方形,左上角坐标为(10, 10),宽和高都为100。

CanvasRenderingContext2D.strokeRect()用来绘制一个矩形区域的边框。

1
2
3
4
5
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.strokeStyle = 'green';
ctx.strokeRect(10, 10, 100, 100);

上面代码绘制一个绿色的空心正方形,左上角坐标为(10, 10),宽和高都为100。

CanvasRenderingContext2D.clearRect()用于擦除指定矩形区域的像素颜色,等同于把早先的绘制效果都去除。

1
2
3
4
5
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.fillRect(10, 10, 100, 100);
ctx.clearRect(15, 15, 90, 90);

上面代码先绘制一个 100 x 100 的正方形,然后在它的内部擦除 90 x 90 的区域,等同于形成了一个5像素宽度的边框。

弧线

以下方法用于绘制弧形。

  • CanvasRenderingContext2D.arc():通过指定圆心和半径绘制弧形。
  • CanvasRenderingContext2D.arcTo():通过指定两根切线和半径绘制弧形。

CanvasRenderingContext2D.arc()主要用来绘制圆形或扇形。

1
2
3
4
5
// 格式
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)

// 实例
ctx.arc(5, 5, 5, 0, 2 * Math.PI, true)

arc()方法的xy参数是圆心坐标,radius是半径,startAngleendAngle则是扇形的起始角度和终止角度(以弧度表示),anticlockwise表示做图时应该逆时针画(true)还是顺时针画(false),这个参数用来控制扇形的方向(比如上半圆还是下半圆)。

下面是绘制实心圆形的例子。

1
2
3
4
5
6
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.beginPath();
ctx.arc(60, 60, 50, 0, Math.PI * 2, true);
ctx.fill();

上面代码绘制了一个半径50,起始角度为0,终止角度为 2 * PI 的完整的圆。

绘制空心半圆的例子。

1
2
3
4
5
6
7
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.beginPath();
ctx.moveTo(50, 20);
ctx.arc(100, 20, 50, 0, Math.PI, false);
ctx.stroke();

CanvasRenderingContext2D.arcTo()方法主要用来绘制圆弧,需要给出两个点的坐标,当前点与第一个点形成一条直线,第一个点与第二个点形成另一条直线,然后画出与这两根直线相切的弧线。

1
2
3
4
5
6
7
8
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arcTo(50, 50, 100, 0, 25);
ctx.lineTo(100, 0);
ctx.stroke();

上面代码中,arcTo()有5个参数,前两个参数是第一个点的坐标,第三个参数和第四个参数是第二个点的坐标,第五个参数是半径。然后,(0, 0)(50, 50)形成一条直线,然后(50, 50)(100, 0)形成第二条直线。弧线就是与这两根直线相切的部分。

文本

以下方法和属性用于绘制文本。

  • CanvasRenderingContext2D.fillText():在指定位置绘制实心字符。
  • CanvasRenderingContext2D.strokeText():在指定位置绘制空心字符。
  • CanvasRenderingContext2D.measureText():返回一个 TextMetrics 对象。
  • CanvasRenderingContext2D.font:指定字型大小和字体,默认值为10px sans-serif
  • CanvasRenderingContext2D.textAlign:文本的对齐方式,默认值为start
  • CanvasRenderingContext2D.direction:文本的方向,默认值为inherit
  • CanvasRenderingContext2D.textBaseline:文本的垂直位置,默认值为alphabetic

fillText()方法用来在指定位置绘制实心字符。

1
CanvasRenderingContext2D.fillText(text, x, y [, maxWidth])

该方法接受四个参数。

  • text:所要填充的字符串。
  • x:文字起点的横坐标,单位像素。
  • y:文字起点的纵坐标,单位像素。
  • maxWidth:文本的最大像素宽度。该参数可选,如果省略,则表示宽度没有限制。如果文本实际长度超过这个参数指定的值,那么浏览器将尝试用较小的字体填充。
1
2
3
4
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.fillText('Hello world', 50, 50);

上面代码在(50, 50)位置写入字符串Hello world

注意,fillText()方法不支持文本断行,所有文本一定出现在一行内。如果要生成多行文本,只有调用多次fillText()方法。

strokeText()方法用来添加空心字符,它的参数与fillText()一致。

1
2
3
4
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.strokeText('Hello world', 50, 50);

上面这两种方法绘制的文本,默认都是10px大小、sans-serif字体,font属性可以改变字体设置。该属性的值是一个字符串,使用 CSS 的font属性即可。

1
2
3
4
5
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.font = 'Bold 20px Arial';
ctx.fillText('Hello world', 50, 50);

textAlign属性用来指定文本的对齐方式。它可以取以下几个值。

  • left:左对齐
  • right:右对齐
  • center:居中
  • start:默认值,起点对齐(从左到右的文本为左对齐,从右到左的文本为右对齐)。
  • end:结尾对齐(从左到右的文本为右对齐,从右到左的文本为左对齐)。
1
2
3
4
5
6
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.font = 'Bold 20px Arial';
ctx.textAlign = 'center';
ctx.fillText('Hello world', 50, 50);

direction属性指定文本的方向,默认值为inherit,表示继承<canvas>document的设置。其他值包括ltr(从左到右)和rtl(从右到左)。

textBaseline属性指定文本的垂直位置,可以取以下值。

  • top:上部对齐(字母的基线是整体上移)。
  • hanging:悬挂对齐(字母的上沿在一根直线上),适用于印度文和藏文。
  • middle:中部对齐(字母的中线在一根直线上)。
  • alphabetic:默认值,表示字母位于字母表的正常位置(四线格的第三根线)。
  • ideographic:下沿对齐(字母的下沿在一根直线上),使用于东亚文字。
  • bottom:底部对齐(字母的基线下移)。对于英文字母,这个设置与ideographic没有差异。

measureText()方法接受一个字符串作为参数,返回一个 TextMetrics 对象,可以从这个对象上面获取参数字符串的信息,目前主要是文本渲染后的宽度(width)。

1
2
3
4
5
6
7
8
9
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var text1 = ctx.measureText('Hello world');
text1.width // 49.46

ctx.font = 'Bold 20px Arial';
var text2 = ctx.measureText('Hello world');
text2.width // 107.78

上面代码中,10px大小的字符串Hello world,渲染后宽度为49.46。放大到20px以后,宽度为107.78

渐变色和图像填充

以下方法用于设置渐变效果和图像填充效果。

  • CanvasRenderingContext2D.createLinearGradient():定义线性渐变样式。
  • CanvasRenderingContext2D.createRadialGradient():定义辐射渐变样式。
  • CanvasRenderingContext2D.createPattern():定义图像填充样式。

createLinearGradient()方法按照给定直线,生成线性渐变的样式。

1
ctx.createLinearGradient(x0, y0, x1, y1)

ctx.createLinearGradient(x0, y0, x1, y1)方法接受四个参数:x0y0是起点的横坐标和纵坐标,x1y1是终点的横坐标和纵坐标。通过不同的坐标值,可以生成从上至下、从左到右的渐变等等。

该方法的返回值是一个CanvasGradient对象,该对象只有一个addColorStop()方向,用来指定渐变点的颜色。addColorStop()方法接受两个参数,第一个参数是0到1之间的一个位置量,0表示起点,1表示终点,第二个参数是一个字符串,表示 CSS 颜色。

1
2
3
4
5
6
7
8
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var gradient = ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, 'green');
gradient.addColorStop(1, 'white');
ctx.fillStyle = gradient;
ctx.fillRect(10, 10, 200, 100);

上面代码中,定义了渐变样式gradient以后,将这个样式指定给fillStyle属性,然后fillRect()就会生成以这个样式填充的矩形区域。

createRadialGradient()方法定义一个辐射渐变,需要指定两个圆。

1
ctx.createRadialGradient(x0, y0, r0, x1, y1, r1)

createRadialGradient()方法接受六个参数,x0y0是辐射起始的圆的圆心坐标,r0是起始圆的半径,x1y1是辐射终止的圆的圆心坐标,r1是终止圆的半径。

该方法的返回值也是一个CanvasGradient对象。

1
2
3
4
5
6
7
8
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var gradient = ctx.createRadialGradient(100, 100, 50, 100, 100, 100);
gradient.addColorStop(0, 'white');
gradient.addColorStop(1, 'green');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 200);

上面代码中,生成辐射样式以后,用这个样式填充一个矩形。

createPattern()方法定义一个图像填充样式,在指定方向上不断重复该图像,填充指定的区域。

1
ctx.createPattern(image, repetition)

该方法接受两个参数,第一个参数是图像数据,它可以是<img>元素,也可以是另一个<canvas>元素,或者一个表示图像的 Blob 对象。第二个参数是一个字符串,有四个可能的值,分别是repeat(双向重复)、repeat-x(水平重复)、repeat-y(垂直重复)、no-repeat(不重复)。如果第二个参数是空字符串或null,则等同于null

该方法的返回值是一个CanvasPattern对象。

1
2
3
4
5
6
7
8
9
10
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var img = new Image();
img.src = 'https://example.com/pattern.png';
img.onload = function( ) {
var pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 400, 400);
};

上面代码中,图像加载成功以后,使用createPattern()生成图像样式,然后使用这个样式填充指定区域。

阴影

以下属性用于设置阴影。

  • CanvasRenderingContext2D.shadowBlur:阴影的模糊程度,默认为0
  • CanvasRenderingContext2D.shadowColor:阴影的颜色,默认为black
  • CanvasRenderingContext2D.shadowOffsetX:阴影的水平位移,默认为0
  • CanvasRenderingContext2D.shadowOffsetY:阴影的垂直位移,默认为0

下面是一个例子。

1
2
3
4
5
6
7
8
9
10
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur = 5;
ctx.shadowColor = 'rgba(0,0,0,0.5)';

ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 100, 100);

Canvas API:图像处理

CanvasRenderingContext2D.drawImage()

Canvas API 允许将图像文件写入画布,做法是读取图片后,使用drawImage()方法将这张图片放上画布。

CanvasRenderingContext2D.drawImage()有三种使用格式。

1
2
3
ctx.drawImage(image, dx, dy);
ctx.drawImage(image, dx, dy, dWidth, dHeight);
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

各个参数的含义如下。

  • image:图像元素
  • sx:图像内部的横坐标,用于映射到画布的放置点上。
  • sy:图像内部的纵坐标,用于映射到画布的放置点上。
  • sWidth:图像在画布上的宽度,会产生缩放效果。如果未指定,则图像不会缩放,按照实际大小占据画布的宽度。
  • sHeight:图像在画布上的高度,会产生缩放效果。如果未指定,则图像不会缩放,按照实际大小占据画布的高度。
  • dx:画布内部的横坐标,用于放置图像的左上角
  • dy:画布内部的纵坐标,用于放置图像的右上角
  • dWidth:图像在画布内部的宽度,会产生缩放效果。
  • dHeight:图像在画布内部的高度,会产生缩放效果。

下面是最简单的使用场景,将图像放在画布上,两者左上角对齐。

1
2
3
4
5
6
7
8
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var img = new Image();
img.src = 'image.png';
img.onload = function () {
ctx.drawImage(img, 0, 0);
};

上面代码将一个 PNG 图像放入画布。这时,图像将是原始大小,如果画布小于图像,就会只显示出图像左上角,正好等于画布大小的那一块。

如果要显示完整的图片,可以用图像的宽和高,设置成画布的宽和高。

1
2
3
4
5
6
7
8
9
10
11
12
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var image = new Image(60, 45);
image.onload = drawImageActualSize;
image.src = 'https://example.com/image.jpg';

function drawImageActualSize() {
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
ctx.drawImage(this, 0, 0, this.naturalWidth, this.naturalHeight);
}

上面代码中,<canvas>元素的大小设置成图像的本来大小,就能保证完整展示图像。由于图像的本来大小,只有图像加载成功以后才能拿到,因此调整画布的大小,必须放在image.onload这个监听函数里面。

像素读写

以下三个方法与像素读写相关。

  • CanvasRenderingContext2D.getImageData():将画布读取成一个 ImageData 对象
  • CanvasRenderingContext2D.putImageData():将 ImageData 对象写入画布
  • CanvasRenderingContext2D.createImageData():生成 ImageData 对象

(1)getImageData()

CanvasRenderingContext2D.getImageData()方法用来读取<canvas>的内容,返回一个 ImageData 对象,包含了每个像素的信息。

1
ctx.getImageData(sx, sy, sw, sh)

getImageData()方法接受四个参数。sxsy是读取区域的左上角坐标,swsh是读取区域的宽度和高度。如果想要读取整个<canvas>区域,可以写成下面这样。

1
2
3
4
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

getImageData()方法返回的是一个ImageData对象。该对象有三个属性。

  • ImageData.data:一个一维数组。该数组的值,依次是每个像素的红、绿、蓝、alpha 通道值(每个值的范围是 0~255),因此该数组的长度等于图像的像素宽度 x 图像的像素高度 x 4。这个数组不仅可读,而且可写,因此通过操作这个数组,就可以达到操作图像的目的。
  • ImageData.width:浮点数,表示 ImageData 的像素宽度。
  • ImageData.height:浮点数,表示 ImageData 的像素高度。

(2)putImageData()

CanvasRenderingContext2D.putImageData()方法将ImageData对象的像素绘制在<canvas>画布上。该方法有两种使用格式。

1
2
ctx.putImageData(imagedata, dx, dy)
ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight)

该方法有如下参数。

  • imagedata:包含像素信息的 ImageData 对象。
  • dx:<canvas>元素内部的横坐标,用于放置 ImageData 图像的左上角。
  • dy:<canvas>元素内部的纵坐标,用于放置 ImageData 图像的左上角。
  • dirtyX:ImageData 图像内部的横坐标,用于作为放置到<canvas>的矩形区域的左上角的横坐标,默认为0。
  • dirtyY:ImageData 图像内部的纵坐标,用于作为放置到<canvas>的矩形区域的左上角的纵坐标,默认为0。
  • dirtyWidth:放置到<canvas>的矩形区域的宽度,默认为 ImageData 图像的宽度。
  • dirtyHeight:放置到<canvas>的矩形区域的高度,默认为 ImageData 图像的高度。

下面是将 ImageData 对象绘制到<canvas>的例子。

1
ctx.putImageData(imageData, 0, 0);

(3)createImageData()

CanvasRenderingContext2D.createImageData()方法用于生成一个空的ImageData对象,所有像素都是透明的黑色(即每个值都是0)。该方法有两种使用格式。

1
2
ctx.createImageData(width, height)
ctx.createImageData(imagedata)

createImageData()方法的参数如下。

  • width:ImageData 对象的宽度,单位为像素。
  • height:ImageData 对象的高度,单位为像素。
  • imagedata:一个现有的 ImageData 对象,返回值将是这个对象的拷贝。
1
2
3
4
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var imageData = ctx.createImageData(100, 100);

上面代码中,imageData是一个 100 x 100 的像素区域,其中每个像素都是透明的黑色。

CanvasRenderingContext2D.save(),CanvasRenderingContext2D.restore()

CanvasRenderingContext2D.save()方法用于将画布的当前样式保存到堆栈,相当于在内存之中产生一个样式快照。

1
2
3
4
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.save();

上面代码中,save()会为画布的默认样式产生一个快照。

CanvasRenderingContext2D.restore()方法将画布的样式恢复到上一个保存的快照,如果没有已保存的快照,则不产生任何效果。

上下文环境,restore方法用于恢复到上一次保存的上下文环境。

1
2
3
4
5
6
7
8
9
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.save();

ctx.fillStyle = 'green';
ctx.restore();

ctx.fillRect(10, 10, 100, 100);

上面代码画一个矩形。矩形的填充色本来设为绿色,但是restore()方法撤销了这个设置,将样式恢复上一次保存的状态(即默认样式),所以实际的填充色是黑色(默认颜色)。

CanvasRenderingContext2D.canvas

CanvasRenderingContext2D.canvas属性指向当前CanvasRenderingContext2D对象所在的<canvas>元素。该属性只读。

1
2
3
4
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.canvas === canvas // true

图像变换

以下方法用于图像变换。

  • CanvasRenderingContext2D.rotate():图像旋转
  • CanvasRenderingContext2D.scale():图像缩放
  • CanvasRenderingContext2D.translate():图像平移
  • CanvasRenderingContext2D.transform():通过一个变换矩阵完成图像变换
  • CanvasRenderingContext2D.setTransform():取消前面的图像变换

(1)rotate()

CanvasRenderingContext2D.rotate()方法用于图像旋转。它接受一个弧度值作为参数,表示顺时针旋转的度数。

1
2
3
4
5
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.rotate(45 * Math.PI / 180);
ctx.fillRect(70, 0, 100, 30);

上面代码会显示一个顺时针倾斜45度的矩形。注意,rotate()方法必须在fillRect()方法之前调用,否则是不起作用的。

旋转中心点始终是画布左上角的原点。如果要更改中心点,需要使用translate()方法移动画布。

(2)scale()

CanvasRenderingContext2D.scale()方法用于缩放图像。它接受两个参数,分别是x轴方向的缩放因子和y轴方向的缩放因子。默认情况下,一个单位就是一个像素,缩放因子可以缩放单位,比如缩放因子0.5表示将大小缩小为原来的50%,缩放因子10表示放大十倍。

1
2
3
4
5
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.scale(10, 3);
ctx.fillRect(10, 10, 10, 10);

上面代码中,原来的矩形是 10 x 10,缩放后展示出来是 100 x 30。

如果缩放因子为1,就表示图像没有任何缩放。如果为-1,则表示方向翻转。ctx.scale(-1, 1)为水平翻转,ctx.scale(1, -1)表示垂直翻转。

1
2
3
4
5
6
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.scale(1, -2);
ctx.font = "16px serif";
ctx.fillText('Hello world!', 20, -20);

上面代码会显示一个水平倒转的、高度放大2倍的Hello World!

注意,负向缩放本质是坐标翻转,所针对的坐标轴就是画布左上角原点的坐标轴。

(3)translate()

CanvasRenderingContext2D.translate()方法用于平移图像。它接受两个参数,分别是 x 轴和 y 轴移动的距离(单位像素)。

1
2
3
4
5
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.translate(50, 50);
ctx.fillRect(0, 0, 100, 100);

(4)transform()

CanvasRenderingContext2D.transform()方法接受一个变换矩阵的六个元素作为参数,完成缩放、旋转、移动和倾斜等变形。

它的使用格式如下。

1
2
3
4
5
6
7
8
9
ctx.transform(a, b, c, d, e, f);
/*
a:水平缩放(默认值1,单位倍数)
b:水平倾斜(默认值0,单位弧度)
c:垂直倾斜(默认值0,单位弧度)
d:垂直缩放(默认值1,单位倍数)
e:水平位移(默认值0,单位像素)
f:垂直位移(默认值0,单位像素)
*/

下面是一个例子。

1
2
3
4
5
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.transform(2, 0, 0, 1, 50, 50);
ctx.fillRect(0, 0, 100, 100);

上面代码中,原始图形是 100 x 100 的矩形,结果缩放成 200 x 100 的矩形,并且左上角从(0, 0)移动到(50, 50)

注意,多个transform()方法具有叠加效果。

(5)setTransform()

CanvasRenderingContext2D.setTransform()方法取消前面的图形变换,将画布恢复到该方法指定的状态。该方法的参数与transform()方法完全一致。

1
2
3
4
5
6
7
8
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

ctx.translate(50, 50);
ctx.fillRect(0, 0, 100, 100);

ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillRect(0, 0, 100, 100);

上面代码中,第一个fillRect()方法绘制的矩形,左上角从(0, 0)平移到(50, 50)setTransform()方法取消了这个变换(已绘制的图形不受影响),将画布恢复到默认状态(变换矩形1, 0, 0, 1, 0, 0),所以第二个矩形的左上角回到(0, 0)

<canvas> 元素的方法

除了CanvasRenderingContext2D对象提供的方法,<canvas>元素本身也有自己的方法。

HTMLCanvasElement.toDataURL()

<canvas>元素的toDataURL()方法,可以将 Canvas 数据转为 Data URI 格式的图像。

1
canvas.toDataURL(type, quality)

toDataURL()方法接受两个参数。

  • type:字符串,表示图像的格式。默认为image/png,另一个可用的值是image/jpeg,Chrome 浏览器还可以使用image/webp
  • quality:浮点数,0到1之间,表示 JPEG 和 WebP 图像的质量系数,默认值为0.92。

该方法的返回值是一个 Data URI 格式的字符串。

1
2
3
4
5
function convertCanvasToImage(canvas) {
var image = new Image();
image.src = canvas.toDataURL('image/png');
return image;
}

上面的代码将<canvas>元素,转化成PNG Data URI。

1
2
3
var fullQuality = canvas.toDataURL('image/jpeg', 0.9);
var mediumQuality = canvas.toDataURL('image/jpeg', 0.6);
var lowQuality = canvas.toDataURL('image/jpeg', 0.3);

上面代码将<canvas>元素转成高画质、中画质、低画质三种 JPEG 图像。

HTMLCanvasElement.toBlob()

HTMLCanvasElement.toBlob()方法用于将<canvas>图像转成一个 Blob 对象,默认类型是image/png。它的使用格式如下。

1
2
3
4
5
// 格式
canvas.toBlob(callback, mimeType, quality)

// 示例
canvas.toBlob(function (blob) {...}, 'image/jpeg', 0.95)

toBlob()方法可以接受三个参数。

  • callback:回调函数。它接受生成的 Blob 对象作为参数。
  • mimeType:字符串,图像的 MIMEType 类型,默认是image/png
  • quality:浮点数,0到1之间,表示图像的质量,只对image/jpegimage/webp类型的图像有效。

注意,该方法没有返回值。

下面的例子将<canvas>图像复制成<img>图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var canvas = document.getElementById('myCanvas');

function blobToImg(blob) {
var newImg = document.createElement('img');
var url = URL.createObjectURL(blob);

newImg.onload = function () {
// 使用完毕,释放 URL 对象
URL.revokeObjectURL(url);
};

newImg.src = url;
document.body.appendChild(newImg);
}

canvas.toBlob(blobToImg);

Canvas 使用实例

动画效果

通过改变坐标,很容易在画布 Canvas 元素上产生动画效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var posX = 20;
var posY = 100;

setInterval(function () {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);

posX += 1;
posY += 0.25;

ctx.beginPath();
ctx.fillStyle = 'white';

ctx.arc(posX, posY, 10, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
}, 30);

上面代码会产生一个小圆点,每隔30毫秒就向右下方移动的效果。setInterval()函数的一开始,之所以要将画布重新渲染黑色底色,是为了抹去上一步的小圆点。

在这个例子的基础上,通过设置圆心坐标,可以产生各种运动轨迹。下面是先上升后下降的例子。

1
2
3
4
5
6
7
8
9
10
var vx = 10;
var vy = -10;
var gravity = 1;

setInterval(function () {
posX += vx;
posY += vy;
vy += gravity;
// ...
});

上面代码中,x坐标始终增大,表示持续向右运动。y坐标先变小,然后在重力作用下,不断增大,表示先上升后下降。

像素处理

通过getImageData()方法和putImageData()方法,可以处理每个像素,进而操作图像内容,因此可以改写图像。

下面是图像处理的通用写法。

1
2
3
4
5
if (canvas.width > 0 && canvas.height > 0) {
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
filter(imageData);
context.putImageData(imageData, 0, 0);
}

上面代码中,filter是一个处理像素的函数。以下是几种常见的filter

(1)灰度效果

灰度图(grayscale)就是取红、绿、蓝三个像素值的算术平均值,这实际上将图像转成了黑白形式。

1
2
3
4
5
6
7
8
9
10
grayscale = function (pixels) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
var r = d[i];
var g = d[i + 1];
var b = d[i + 2];
d[i] = d[i + 1] = d[i + 2] = (r + g + b) / 3;
}
return pixels;
};

上面代码中,d[i]是红色值,d[i+1]是绿色值,d[i+2]是蓝色值,d[i+3]是 alpha 通道值。转成灰度的算法,就是将红、绿、蓝三个值相加后除以3,再将结果写回数组。

(2)复古效果

复古效果(sepia)是将红、绿、蓝三种值,分别取这三个值的某种加权平均值,使得图像有一种古旧的效果。

1
2
3
4
5
6
7
8
9
10
11
12
sepia = function (pixels) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
var r = d[i];
var g = d[i + 1];
var b = d[i + 2];
d[i] = (r * 0.393) + (g * 0.769) + (b * 0.189); // red
d[i + 1] = (r * 0.349) + (g * 0.686) + (b * 0.168); // green
d[i + 2] = (r * 0.272) + (g * 0.534) + (b * 0.131); // blue
}
return pixels;
};

(3)红色蒙版效果

红色蒙版指的是,让图像呈现一种偏红的效果。算法是将红色通道设为红、绿、蓝三个值的平均值,而将绿色通道和蓝色通道都设为0。

1
2
3
4
5
6
7
8
9
10
11
var red = function (pixels) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
var r = d[i];
var g = d[i + 1];
var b = d[i + 2];
d[i] = (r + g + b)/3; // 红色通道取平均值
d[i + 1] = d[i + 2] = 0; // 绿色通道和蓝色通道都设为0
}
return pixels;
};

(4)亮度效果

亮度效果(brightness)是指让图像变得更亮或更暗。算法将红色通道、绿色通道、蓝色通道,同时加上一个正值或负值。

1
2
3
4
5
6
7
8
9
var brightness = function (pixels, delta) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
d[i] += delta; // red
d[i + 1] += delta; // green
d[i + 2] += delta; // blue
}
return pixels;
};

(5)反转效果

反转效果(invert)是指图片呈现一种色彩颠倒的效果。算法为红、绿、蓝通道都取各自的相反值(255 - 原值)。

1
2
3
4
5
6
7
8
9
invert = function (pixels) {
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
d[i] = 255 - d[i];
d[i + 1] = 255 - d[i + 1];
d[i + 2] = 255 - d[i + 2];
}
return pixels;
};

参考链接

剪贴板操作 Clipboard API 教程

简介

浏览器允许 JavaScript 脚本读写剪贴板,自动复制或粘贴内容。

一般来说,脚本不应该改动用户的剪贴板,以免不符合用户的预期。但是,有些时候这样做确实能够带来方便,比如“一键复制”功能,用户点击一下按钮,指定的内容就自动进入剪贴板。

目前,一共有三种方法可以实现剪贴板操作。

  • Document.execCommand()方法
  • 异步的 Clipboard API
  • copy事件和paste事件

本文逐一介绍这三种方法。

Document.execCommand() 方法

Document.execCommand()是操作剪贴板的传统方法,各种浏览器都支持。

它支持复制、剪切和粘贴这三个操作。

  • document.execCommand('copy')(复制)
  • document.execCommand('cut')(剪切)
  • document.execCommand('paste')(粘贴)

(1)复制操作

复制时,先选中文本,然后调用document.execCommand('copy'),选中的文本就会进入剪贴板。

1
2
3
const inputElement = document.querySelector('#input');
inputElement.select();
document.execCommand('copy');

上面示例中,脚本先选中输入框inputElement里面的文字(inputElement.select()),然后document.execCommand('copy')将其复制到剪贴板。

注意,复制操作最好放在事件监听函数里面,由用户触发(比如用户点击按钮)。如果脚本自主执行,某些浏览器可能会报错。

(2)粘贴操作

粘贴时,调用document.execCommand('paste'),就会将剪贴板里面的内容,输出到当前的焦点元素中。

1
2
3
const pasteText = document.querySelector('#output');
pasteText.focus();
document.execCommand('paste');

(3)缺点

Document.execCommand()方法虽然方便,但是有一些缺点。

首先,它只能将选中的内容复制到剪贴板,无法向剪贴板任意写入内容。

其次,它是同步操作,如果复制/粘贴大量数据,页面会出现卡顿。有些浏览器还会跳出提示框,要求用户许可,这时在用户做出选择前,页面会失去响应。

为了解决这些问题,浏览器厂商提出了异步的 Clipboard API。

异步 Clipboard API

Clipboard API 是下一代的剪贴板操作方法,比传统的document.execCommand()方法更强大、更合理。

它的所有操作都是异步的,返回 Promise 对象,不会造成页面卡顿。而且,它可以将任意内容(比如图片)放入剪贴板。

navigator.clipboard属性返回 Clipboard 对象,所有操作都通过这个对象进行。

1
const clipboardObj = navigator.clipboard;

如果navigator.clipboard属性返回undefined,就说明当前浏览器不支持这个 API。

由于用户可能把敏感数据(比如密码)放在剪贴板,允许脚本任意读取会产生安全风险,所以这个 API 的安全限制比较多。

首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(localhost)允许使用非加密协议。

其次,调用时需要明确获得用户的许可。权限的具体实现使用了 Permissions API,跟剪贴板相关的有两个权限:clipboard-write(写权限)和clipboard-read(读权限)。“写权限”自动授予脚本,而“读权限”必须用户明确同意给予。也就是说,写入剪贴板,脚本可以自动完成,但是读取剪贴板时,浏览器会弹出一个对话框,询问用户是否同意读取。

另外,需要注意的是,脚本读取的总是当前页面的剪贴板。这带来的一个问题是,如果把相关的代码粘贴到开发者工具中直接运行,可能会报错,因为这时的当前页面是开发者工具的窗口,而不是网页页面。

1
2
3
4
(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
})();

如果你把上面的代码,粘贴到开发者工具里面运行,就会报错。因为代码运行的时候,开发者工具窗口是当前页,这个页面不存在 Clipboard API 依赖的 DOM 接口。一个解决方法就是,相关代码放到setTimeout()里面延迟运行,在调用函数之前快速点击浏览器的页面窗口,将其变成当前页。

1
2
3
4
setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);

上面代码粘贴到开发者工具运行后,快速点击一下网页的页面窗口,使其变为当前页,这样就不会报错了。

Clipboard 对象

Clipboard 对象提供了四个方法,用来读写剪贴板。它们都是异步方法,返回 Promise 对象。

Clipboard.readText()

Clipboard.readText()方法用于复制剪贴板里面的文本数据。

1
2
3
4
5
6
7
document.body.addEventListener(
'click',
async (e) => {
const text = await navigator.clipboard.readText();
console.log(text);
}
)

上面示例中,用户点击页面后,就会输出剪贴板里面的文本。注意,浏览器这时会跳出一个对话框,询问用户是否同意脚本读取剪贴板。

如果用户不同意,脚本就会报错。这时,可以使用try...catch结构,处理报错。

1
2
3
4
5
6
7
8
async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted content: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}

Clipboard.read()

Clipboard.read()方法用于复制剪贴板里面的数据,可以是文本数据,也可以是二进制数据(比如图片)。该方法需要用户明确给予许可。

该方法返回一个 Promise 对象。一旦该对象的状态变为 resolved,就可以获得一个数组,每个数组成员都是 ClipboardItem 对象的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
async function getClipboardContents() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
console.log(URL.createObjectURL(blob));
}
}
} catch (err) {
console.error(err.name, err.message);
}
}

ClipboardItem 对象表示一个单独的剪贴项,每个剪贴项都拥有ClipboardItem.types属性和ClipboardItem.getType()方法。

ClipboardItem.types属性返回一个数组,里面的成员是该剪贴项可用的 MIME 类型,比如某个剪贴项可以用 HTML 格式粘贴,也可以用纯文本格式粘贴,那么它就有两个 MIME 类型(text/htmltext/plain)。

ClipboardItem.getType(type)方法用于读取剪贴项的数据,返回一个 Promise 对象。该方法接受剪贴项的 MIME 类型作为参数,返回该类型的数据,该参数是必需的,否则会报错。

Clipboard.writeText()

Clipboard.writeText()方法用于将文本内容写入剪贴板。

1
2
3
4
5
6
document.body.addEventListener(
'click',
async (e) => {
await navigator.clipboard.writeText('Yo')
}
)

上面示例是用户在网页点击后,脚本向剪贴板写入文本数据。

该方法不需要用户许可,但是最好也放在try...catch里面防止报错。

1
2
3
4
5
6
7
8
async function copyPageUrl() {
try {
await navigator.clipboard.writeText(location.href);
console.log('Page URL copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}

Clipboard.write()

Clipboard.write()方法用于将任意数据写入剪贴板,可以是文本数据,也可以是二进制数据。

该方法接受一个 ClipboardItem 实例作为参数,表示写入剪贴板的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
const imgURL = 'https://dummyimage.com/300.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}

上面示例中,脚本向剪贴板写入了一张图片。注意,Chrome 浏览器目前只支持写入 PNG 格式的图片。

ClipboardItem()是浏览器原生提供的构造函数,用来生成ClipboardItem实例,它接受一个对象作为参数,该对象的键名是数据的 MIME 类型,键值就是数据本身。

下面的例子是将同一个剪贴项的多种格式的值,写入剪贴板,一种是文本数据,另一种是二进制数据,供不同的场合粘贴使用。

1
2
3
4
5
6
7
8
9
function copy() {
const image = await fetch('kitten.png');
const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
const item = new ClipboardItem({
'text/plain': text,
'image/png': image
});
await navigator.clipboard.write([item]);
}

copy 事件,cut 事件

用户向剪贴板放入数据时,将触发copy事件。

下面的示例是将用户放入剪贴板的文本,转为大写。

1
2
3
4
5
6
7
const source = document.querySelector('.source');

source.addEventListener('copy', (event) => {
const selection = document.getSelection();
event.clipboardData.setData('text/plain', selection.toString().toUpperCase());
event.preventDefault();
});

上面示例中,事件对象的clipboardData属性包含了剪贴板数据。它是一个对象,有以下属性和方法。

  • Event.clipboardData.setData(type, data):修改剪贴板数据,需要指定数据类型。
  • Event.clipboardData.getData(type):获取剪贴板数据,需要指定数据类型。
  • Event.clipboardData.clearData([type]):清除剪贴板数据,可以指定数据类型。如果不指定类型,将清除所有类型的数据。
  • Event.clipboardData.items:一个类似数组的对象,包含了所有剪贴项,不过通常只有一个剪贴项。

下面的示例是拦截用户的复制操作,将指定内容放入剪贴板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const clipboardItems = [];

document.addEventListener('copy', async (e) => {
e.preventDefault();
try {
let clipboardItems = [];
for (const item of e.clipboardData.items) {
if (!item.type.startsWith('image/')) {
continue;
}
clipboardItems.push(
new ClipboardItem({
[item.type]: item,
})
);
await navigator.clipboard.write(clipboardItems);
console.log('Image copied.');
}
} catch (err) {
console.error(err.name, err.message);
}
});

上面示例中,先使用e.preventDefault()取消了剪贴板的默认操作,然后由脚本接管复制操作。

cut事件则是在用户进行剪切操作时触发,它的处理跟copy事件完全一样,也是从Event.clipboardData属性拿到剪切的数据。

paste 事件

用户使用剪贴板数据,进行粘贴操作时,会触发paste事件。

下面的示例是拦截粘贴操作,由脚本将剪贴板里面的数据取出来。

1
2
3
4
5
document.addEventListener('paste', async (e) => {
e.preventDefault();
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
});

参考链接

Fetch API 教程

fetch()是 XMLHttpRequest 的升级版,用于在 JavaScript 脚本里面发出 HTTP 请求。

浏览器原生提供这个对象。本章详细介绍它的用法。

基本用法

fetch()的功能与 XMLHttpRequest 基本相同,都是向服务器发出 HTTP 请求,但有三个主要的差异。

(1)fetch()使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁。

(2)fetch()采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码。

(3)fetch()通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHttpRequest 对象不支持数据流,所有的数据全部放在缓存里,不支持分块读取,必须等待全部获取后,再一次性读取。

用法上,fetch()接受一个 URL 字符串作为参数,默认向该网址发出 GET 请求,返回一个 Promise 对象。它的基本用法如下。

1
2
3
fetch(url)
.then(...)
.catch(...)

下面是一个例子,从服务器获取 JSON 数据。

1
2
3
4
fetch('https://api.github.com/users/ruanyf')
.then(response => response.json())
.then(json => console.log(json))
.catch(err => console.log('Request Failed', err));

上面示例中,fetch()接收到的response是一个 Stream 对象,里面的数据本例是 JSON 数据,所以使用response.json()方法,将其转为 JSON 对象。它是一个异步操作,返回一个 Promise 对象。

Promise 可以使用 await 语法改写,使得语义更清晰。

1
2
3
4
5
6
7
8
9
async function getJSON() {
let url = 'https://api.github.com/users/ruanyf';
try {
let response = await fetch(url);
return await response.json();
} catch (error) {
console.log('Request Failed', error);
}
}

上面示例中,await语句必须放在try...catch里面,这样才能捕捉异步操作中可能发生的错误。

后文都采用await的写法,不再使用.then()的写法。

Response 对象:处理 HTTP 回应

Response 对象的同步属性

fetch()请求成功以后,得到的是一个 Response 对象。它对应服务器的 HTTP 回应。

1
const response = await fetch(url);

前面说过,Response 包含的数据通过 Stream 接口异步读取,但是它还包含一些同步属性,对应 HTTP 回应的标头信息(Headers),可以立即读取。

1
2
3
4
5
async function fetchText() {
let response = await fetch('/readme.txt');
console.log(response.status);
console.log(response.statusText);
}

上面示例中,response.statusresponse.statusText就是 Response 的同步属性,可以立即读取。

标头信息属性有下面这些。

Response.ok

Response.ok属性返回一个布尔值,表示请求是否成功,true对应 HTTP 请求的状态码 200 到 299,false对应其他的状态码。

Response.status

Response.status属性返回一个数字,表示 HTTP 回应的状态码(例如200,表示成功请求)。

Response.statusText

Response.statusText属性返回一个字符串,表示 HTTP 回应的状态信息(例如请求成功以后,服务器返回“OK”)。

Response.url

Response.url属性返回请求的 URL。如果 URL 存在跳转,该属性返回的是最终 URL。

Response.type

Response.type属性返回请求的类型。可能的值如下:

  • basic:普通请求,即同源请求。
  • cors:跨源请求。
  • error:网络错误,主要用于 Service Worker。
  • opaque:如果fetch()请求的type属性设为no-cors,就会返回这个值,详见请求部分。表示发出的是简单的跨源请求,类似<form>表单的那种跨源请求。
  • opaqueredirect:如果fetch()请求的redirect属性设为manual,就会返回这个值,详见请求部分。

Response.redirected

Response.redirected属性返回一个布尔值,表示请求是否发生过跳转。

判断请求是否成功

fetch()发出请求以后,有一个很重要的注意点:只有网络错误,或者无法连接时,fetch()才会报错,其他情况都不会报错,而是认为请求成功。

这就是说,即使服务器返回的状态码是 4xx 或 5xx,fetch()也不会报错(即 Promise 不会变为 rejected状态)。

只有通过Response.status属性,得到 HTTP 回应的真实状态码,才能判断请求是否成功。请看下面的例子。

1
2
3
4
5
6
7
8
async function fetchText() {
let response = await fetch('/readme.txt');
if (response.status >= 200 && response.status < 300) {
return await response.text();
} else {
throw new Error(response.statusText);
}
}

上面示例中,response.status属性只有等于 2xx (200~299),才能认定请求成功。这里不用考虑网址跳转(状态码为 3xx),因为fetch()会将跳转的状态码自动转为 200。

另一种方法是判断response.ok是否为true

1
2
3
4
5
if (response.ok) {
// 请求成功
} else {
// 请求失败
}

Response.headers 属性

Response 对象还有一个Response.headers属性,指向一个 Headers 对象,对应 HTTP 回应的所有标头。

Headers 对象可以使用for...of循环进行遍历。

1
2
3
4
5
6
7
8
9
10
const response = await fetch(url);

for (let [key, value] of response.headers) {
console.log(`${key} : ${value}`);
}

// 或者
for (let [key, value] of response.headers.entries()) {
console.log(`${key} : ${value}`);
}

Headers 对象提供了以下方法,用来操作标头。

  • Headers.get():根据指定的键名,返回键值。
  • Headers.has(): 返回一个布尔值,表示是否包含某个标头。
  • Headers.set():将指定的键名设置为新的键值,如果该键名不存在则会添加。
  • Headers.append():添加标头。
  • Headers.delete():删除标头。
  • Headers.keys():返回一个遍历器,可以依次遍历所有键名。
  • Headers.values():返回一个遍历器,可以依次遍历所有键值。
  • Headers.entries():返回一个遍历器,可以依次遍历所有键值对([key, value])。
  • Headers.forEach():依次遍历标头,每个标头都会执行一次参数函数。

上面的有些方法可以修改标头,那是因为继承自 Headers 接口。对于 HTTP 回应来说,修改标头意义不大,况且很多标头是只读的,浏览器不允许修改。

这些方法中,最常用的是response.headers.get(),用于读取某个标头的值。

1
2
3
let response =  await  fetch(url);
response.headers.get('Content-Type')
// application/json; charset=utf-8

Headers.keys()Headers.values()方法用来分别遍历标头的键名和键值。

1
2
3
4
5
6
7
8
9
// 键名
for(let key of myHeaders.keys()) {
console.log(key);
}

// 键值
for(let value of myHeaders.values()) {
console.log(value);
}

Headers.forEach()方法也可以遍历所有的键值和键名。

1
2
3
4
let response = await fetch(url);
response.headers.forEach(
(value, key) => console.log(key, ':', value)
);

读取内容的方法

Response对象根据服务器返回的不同类型的数据,提供了不同的读取方法。

  • response.text():得到文本字符串。
  • response.json():得到 JSON 对象。
  • response.blob():得到二进制 Blob 对象。
  • response.formData():得到 FormData 表单对象。
  • response.arrayBuffer():得到二进制 ArrayBuffer 对象。

上面5个读取方法都是异步的,返回的都是 Promise 对象。必须等到异步操作结束,才能得到服务器返回的完整数据。

response.text()

response.text()可以用于获取文本数据,比如 HTML 文件。

1
2
3
const response = await fetch('/users.html');
const body = await response.text();
document.body.innerHTML = body

response.json()

response.json()主要用于获取服务器返回的 JSON 数据,前面已经举过例子了。

response.formData()

response.formData()主要用在 Service Worker 里面,拦截用户提交的表单,修改某些数据以后,再提交给服务器。

response.blob()

response.blob()用于获取二进制文件。

1
2
3
4
5
6
const response = await fetch('flower.jpg');
const myBlob = await response.blob();
const objectURL = URL.createObjectURL(myBlob);

const myImage = document.querySelector('img');
myImage.src = objectURL;

上面示例读取图片文件flower.jpg,显示在网页上。

response.arrayBuffer()

response.arrayBuffer()主要用于获取流媒体文件。

1
2
3
4
5
6
7
8
9
10
const audioCtx = new window.AudioContext();
const source = audioCtx.createBufferSource();

const response = await fetch('song.ogg');
const buffer = await response.arrayBuffer();

const decodeData = await audioCtx.decodeAudioData(buffer);
source.buffer = buffer;
source.connect(audioCtx.destination);
source.loop = true;

上面示例是response.arrayBuffer()获取音频文件song.ogg,然后在线播放的例子。

Response.clone()

Stream 对象只能读取一次,读取完就没了。这意味着,前一节的五个读取方法,只能使用一个,否则会报错。

1
2
let text =  await response.text();
let json = await response.json(); // 报错

上面示例先使用了response.text(),就把 Stream 读完了。后面再调用response.json(),就没有内容可读了,所以报错。

Response 对象提供Response.clone()方法,创建Response对象的副本,实现多次读取。

1
2
3
4
5
6
7
8
const response1 = await fetch('flowers.jpg');
const response2 = response1.clone();

const myBlob1 = await response1.blob();
const myBlob2 = await response2.blob();

image1.src = URL.createObjectURL(myBlob1);
image2.src = URL.createObjectURL(myBlob2);

上面示例中,response.clone()复制了一份 Response 对象,然后将同一张图片读取了两次。

Response 对象还有一个Response.redirect()方法,用于将 Response 结果重定向到指定的 URL。该方法一般只用在 Service Worker 里面,这里就不介绍了。

Response.body 属性

Response.body属性是 Response 对象暴露出的底层接口,返回一个 ReadableStream 对象,供用户操作。

它可以用来分块读取内容,应用之一就是显示下载的进度。

1
2
3
4
5
6
7
8
9
10
11
12
const response = await fetch('flower.jpg');
const reader = response.body.getReader();

while(true) {
const {done, value} = await reader.read();

if (done) {
break;
}

console.log(`Received ${value.length} bytes`)
}

上面示例中,response.body.getReader()方法返回一个遍历器。这个遍历器的read()方法每次返回一个对象,表示本次读取的内容块。

这个对象的done属性是一个布尔值,用来判断有没有读完;value属性是一个 arrayBuffer 数组,表示内容块的内容,而value.length属性是当前块的大小。

fetch()的第二个参数:定制 HTTP 请求

fetch()的第一个参数是 URL,还可以接受第二个参数,作为配置对象,定制发出的 HTTP 请求。

1
fetch(url, optionObj)

上面命令的optionObj就是第二个参数。

HTTP 请求的方法、标头、数据体都在这个对象里面设置。下面是一些示例。

(1)POST 请求

1
2
3
4
5
6
7
8
9
const response = await fetch(url, {
method: 'POST',
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
body: 'foo=bar&lorem=ipsum',
});

const json = await response.json();

上面示例中,配置对象用到了三个属性。

  • method:HTTP 请求的方法,POSTDELETEPUT都在这个属性设置。
  • headers:一个对象,用来定制 HTTP 请求的标头。
  • body:POST 请求的数据体。

注意,有些标头不能通过headers属性设置,比如Content-LengthCookieHost等等。它们是由浏览器自动生成,无法修改。

(2)提交 JSON 数据

1
2
3
4
5
6
7
8
const user =  { name:  'John', surname:  'Smith'  };
const response = await fetch('/article/fetch/post/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(user)
});

上面示例中,标头Content-Type要设成'application/json;charset=utf-8'。因为默认发送的是纯文本,Content-Type的默认值是'text/plain;charset=UTF-8'

(3)提交表单

1
2
3
4
5
6
const form = document.querySelector('form');

const response = await fetch('/users', {
method: 'POST',
body: new FormData(form)
})

(4)文件上传

如果表单里面有文件选择器,可以用前一个例子的写法,上传的文件包含在整个表单里面,一起提交。

另一种方法是用脚本添加文件,构造出一个表单,进行上传,请看下面的例子。

1
2
3
4
5
6
7
8
9
10
const input = document.querySelector('input[type="file"]');

const data = new FormData();
data.append('file', input.files[0]);
data.append('user', 'foo');

fetch('/avatars', {
method: 'POST',
body: data
});

上传二进制文件时,不用修改标头的Content-Type,浏览器会自动设置。

(5)直接上传二进制数据

fetch()也可以直接上传二进制数据,将 Blob 或 arrayBuffer 数据放在body属性里面。

1
2
3
4
5
6
7
8
let blob = await new Promise(resolve =>
canvasElem.toBlob(resolve, 'image/png')
);

let response = await fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
});

fetch()配置对象的完整 API

fetch()第二个参数的完整 API 如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const response = fetch(url, {
method: "GET",
headers: {
"Content-Type": "text/plain;charset=UTF-8"
},
body: undefined,
referrer: "about:client",
referrerPolicy: "no-referrer-when-downgrade",
mode: "cors",
credentials: "same-origin",
cache: "default",
redirect: "follow",
integrity: "",
keepalive: false,
signal: undefined
});

fetch()请求的底层用的是 Request() 对象的接口,参数完全一样,因此上面的 API 也是Request()的 API。

这些属性里面,headersbodymethod前面已经给过示例了,下面是其他属性的介绍。

cache

cache属性指定如何处理缓存。可能的取值如下:

  • default:默认值,先在缓存里面寻找匹配的请求。
  • no-store:直接请求远程服务器,并且不更新缓存。
  • reload:直接请求远程服务器,并且更新缓存。
  • no-cache:将服务器资源跟本地缓存进行比较,有新的版本才使用服务器资源,否则使用缓存。
  • force-cache:缓存优先,只有不存在缓存的情况下,才请求远程服务器。
  • only-if-cached:只检查缓存,如果缓存里面不存在,将返回504错误。

mode

mode属性指定请求的模式。可能的取值如下:

  • cors:默认值,允许跨源请求。
  • same-origin:只允许同源请求。
  • no-cors:请求方法只限于 GET、POST 和 HEAD,并且只能使用有限的几个简单标头,不能添加跨源的复杂标头,相当于提交表单、<script>加载脚本、<img>加载图片等传统的跨源请求方法。

credentials

credentials属性指定是否发送 Cookie。可能的取值如下:

  • same-origin:默认值,同源请求时发送 Cookie,跨源请求时不发送。
  • include:不管同源请求,还是跨源请求,一律发送 Cookie。
  • omit:一律不发送。

跨源请求发送 Cookie,需要将credentials属性设为include

1
2
3
fetch('http://another.com', {
credentials: "include"
});

signal

signal属性指定一个 AbortSignal 实例,用于取消fetch()请求,详见下一节。

keepalive

keepalive属性用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。

一个典型的场景就是,用户离开网页时,脚本向服务器提交一些用户行为的统计信息。这时,如果不用keepalive属性,数据可能无法发送,因为浏览器已经把页面卸载了。

1
2
3
4
5
6
7
8
9
10
11
12
window.onunload = function() {
fetch('/analytics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
some: 'data'
}),
keepalive: true
});
};

redirect

redirect属性指定 HTTP 跳转的处理方法。可能的取值如下:

  • follow:默认值,fetch()跟随 HTTP 跳转。
  • error:如果发生跳转,fetch()就报错。
  • manualfetch()不跟随 HTTP 跳转,但是response.url属性会指向新的 URL,response.redirected属性会变为true,由开发者自己决定后续如何处理跳转。

integrity

integrity属性指定一个哈希值,用于检查 HTTP 回应传回的数据是否等于这个预先设定的哈希值。

比如,下载文件时,检查文件的 SHA-256 哈希值是否相符,确保没有被篡改。

1
2
3
fetch('http://site.com/file', {
integrity: 'sha256-abcdef'
});

referrer

referrer属性用于设定fetch()请求的referer标头。

这个属性可以为任意字符串,也可以设为空字符串(即不发送referer标头)。

1
2
3
fetch('/page', {
referrer: ''
});

referrerPolicy

referrerPolicy属性用于设定Referer标头的规则。可能的取值如下:

  • no-referrer-when-downgrade:默认值,总是发送Referer标头,除非从 HTTPS 页面请求 HTTP 资源时不发送。
  • no-referrer:不发送Referer标头。
  • originReferer标头只包含域名,不包含完整的路径。
  • origin-when-cross-origin:同源请求Referer标头包含完整的路径,跨源请求只包含域名。
  • same-origin:跨源请求不发送Referer,同源请求发送。
  • strict-originReferer标头只包含域名,HTTPS 页面请求 HTTP 资源时不发送Referer标头。
  • strict-origin-when-cross-origin:同源请求时Referer标头包含完整路径,跨源请求时只包含域名,HTTPS 页面请求 HTTP 资源时不发送该标头。
  • unsafe-url:不管什么情况,总是发送Referer标头。

取消fetch()请求

fetch()请求发送以后,如果中途想要取消,需要使用AbortController对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let controller = new AbortController();
let signal = controller.signal;

fetch(url, {
signal: controller.signal
});

signal.addEventListener('abort',
() => console.log('abort!')
);

controller.abort(); // 取消

console.log(signal.aborted); // true

上面示例中,首先新建 AbortController 实例,然后发送fetch()请求,配置对象的signal属性必须指定接收 AbortController 实例发送的信号controller.signal

controller.abort()方法用于发出取消信号。这时会触发abort事件,这个事件可以监听,也可以通过controller.signal.aborted属性判断取消信号是否已经发出。

下面是一个1秒后自动取消请求的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);

try {
let response = await fetch('/long-operation', {
signal: controller.signal
});
} catch(err) {
if (err.name == 'AbortError') {
console.log('Aborted!');
} else {
throw err;
}
}

参考链接

FontFace API

FontFace API 用来控制字体加载。

这个 API 提供一个构造函数FontFace(),返回一个字体对象。

1
new FontFace(family, source, descriptors)

FontFace()构造函数接受三个参数。

  • family:字符串,表示字体名,写法与 CSS 的@font-facefont-family属性相同。
  • source:字体文件的 URL(必须包括 CSS 的url()方法),或者是一个字体的 ArrayBuffer 对象。
  • descriptors:对象,用来定制字体文件。该参数可选。
1
2
3
4
5
6
var fontFace = new FontFace(
'Roboto',
'url(https://fonts.example.com/roboto.woff2)'
);

fontFace.family // "Roboto"

FontFace()返回的是一个字体对象,这个对象包含字体信息。注意,这时字体文件还没有开始加载。

字体对象包含以下属性。

  • FontFace.family:字符串,表示字体的名字,等同于 CSS 的font-family属性。
  • FontFace.display:字符串,指定字体加载期间如何展示,等同于 CSS 的font-display属性。它有五个可能的值:auto(由浏览器决定)、block(字体加载期间,前3秒会显示不出内容,然后只要还没完成加载,就一直显示后备字体)、fallback(前100毫秒显示不出内容,后3秒显示后备字体,然后只要字体还没完成加载,就一直显示后备字体)、optional(前100毫秒显示不出内容,然后只要字体还没有完成加载,就一直显示后备字体),swap(只要字体没有完成加载,就一直显示后备字体)。
  • FontFace.style:字符串,等同于 CSS 的font-style属性。
  • FontFace.weight:字符串,等同于 CSS 的font-weight属性。
  • FontFace.stretch:字符串,等同于 CSS 的font-stretch属性。
  • FontFace.unicodeRange:字符串,等同于descriptors对象的同名属性。
  • FontFace.variant:字符串,等同于descriptors对象的同名属性。
  • FontFace.featureSettings:字符串,等同于descriptors对象的同名属性。
  • FontFace.status:字符串,表示字体的加载状态,有四个可能的值:unloadedloadingloadederror。该属性只读。
  • FontFace.loaded:返回一个 Promise 对象,字体加载成功或失败,会导致该 Promise 状态改变。该属性只读。

字体对象的方法,只有一个FontFace.load(),该方法会真正开始加载字体。它返回一个 Promise 对象,状态由字体加载的结果决定。

1
2
3
4
5
var f = new FontFace('test', 'url(x)');

f.load().then(function () {
// 网页可以开始使用该字体
});

FormData 对象

简介

FormData 代表表单数据,是浏览器的原生对象。

它可以当作构造函数使用,构造一个表单实例。

1
const formData = new FormData();

上面示例中,FormData()当作构造函数使用,返回一个空的表单实例对象。

它也可以接受一个表单的 DOM 节点当作参数,将表单的所有元素及其值,转换成一个个键值对,包含在返回的实例对象里面。

1
const formData = new FormData(form);

上面示例中,FormData()的参数form就是一个表单的 DOM 节点对象。

下面是用法示例,通过脚本发送表单数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form id="formElem">
<input type="text" name="firstName" value="John">
Picture: <input type="file" name="picture" accept="image/*">
<input type="submit">
</form>

<script>
formElem.onsubmit = async (e) => {
e.preventDefault();

let response = await fetch('/article/formdata/post/user-avatar', {
method: 'POST',
body: new FormData(formElem)
});

let result = await response.json();

alert(result.message);
};
</script>

浏览器向服务器发送表单数据时,不管是用户点击 Submit 按钮发送,还是使用脚本发送,都会自动将其编码,并以Content-Type: multipart/form-data的形式发送。

FormData()还有第三种用法,如果想把“提交”(Submit)按钮也加入表单的键值对,可以把按钮的 DOM 节点当作FormData()的第二个参数。

1
new FormData(form, submitter)

上面代码中,submitter就是提交按钮的 DOM 节点。这种用法适合表单有多个提交按钮,服务端需要通过按钮的值来判断,用户到底选用了哪个按钮。

1
2
3
4
5
6
7
8
// 表单有两个提交按钮
// <button name="intent" value="save">Save</button>
// <button name="intent" value="saveAsCopy">Save As Copy</button>

const form = document.getElementById("form");
const submitter = document.querySelector("button[value=save]");

const formData = new FormData(form, submitter);

上面示例中,FormData()加入了第二个参数,实例对象formData就会增加一个键值对,键名为intent,键值为save

实例方法

append()

append()用于添加一个键值对,即添加一个表单元素。它有两种使用形式。

1
2
FormData.append(name, value)
FormData.append(name, blob, fileName)

它的第一个参数是键名,第二个参数是键值。上面的第二种形式FormData.append(name, blob, fileName),相当于添加一个文件选择器<input type="file">,第二个参数blob是文件的二进制内容,第三个参数fileName是文件名。

如果键名已经存在,它会为其添加新的键值,即同一个键名有多个键值。

下面是一个用法示例。

1
2
3
4
5
6
7
8
9
let formData = new FormData();
formData.append('key1', 'value1');
formData.append('key2', 'value2');

for(let [name, value] of formData) {
console.log(`${name} = ${value}`);
}
// key1 = value1
// key2 = value2

下面是添加二进制文件的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// HTML 代码如下
// <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>

let imageBlob = await new Promise(
resolve => canvasElem.toBlob(resolve, 'image/png')
);

let formData = new FormData();
formData.append('image', imageBlob, 'image.png');

let response = await fetch('/article/formdata/post/image-form', {
method: 'POST',
body: formData
});

let result = await response.json();
console.log(result);

下面是添加 XML 文件的例子。

1
2
3
4
const content = '<q id="a"><span id="b">hey!</span></q>';
const blob = new Blob([content], { type: "text/xml" });

formData.append('userfile', blob);

delete()

delete()用于删除指定的键值对,它的参数为键名。

1
FormData.delete(name);

entries()

entries()返回一个迭代器,用于遍历所有键值对。

1
FormData.entries()

下面是一个用法示例。

1
2
3
4
const form = document.querySelector('#subscription');
const formData = new FormData(form);
const values = [...formData.entries()];
console.log(values);

下面是使用entries()遍历键值对的例子。

1
2
3
4
5
6
7
8
formData.append("key1", "value1");
formData.append("key2", "value2");

for (const pair of formData.entries()) {
console.log(`${pair[0]}, ${pair[1]}`);
}
// key1, value1
// key2, value2

get()

get()用于获取指定键名的键值,它的参数为键名。

1
FormData.get(name)

如果该键名有多个键值,只返回第一个键值。如果找不到指定键名,则返回null

getAll()

getAll()用于获取指定键名的所有键值,它的参数为键名,返回值为一个数组。如果找不到指定键名,则返回一个空数组。

1
FormData.getAll(name)

has()

has()返回一个布尔值,表示是否存在指定键名,它的参数为键名。

1
FormData.has(name)

keys()

keys()返回一个键名的迭代器,用于遍历所有键名。

1
FormData.keys()

下面是用法示例。

1
2
3
4
5
6
7
8
9
const formData = new FormData();
formData.append("key1", "value1");
formData.append("key2", "value2");

for (const key of formData.keys()) {
console.log(key);
}
// key1
// key2

set()

set()用于为指定键名设置新的键值。它有两种使用形式。

1
2
FormData.set(name, value);
FormData.set(name, blob, fileName);

它的第一个参数为键名,第二个参数为键值。上面第二种形式为上传文件,第二个参数blob为文件的二进制内容,第三个参数fileName为文件名。该方法没有返回值。

如果指定键名不存在,它会添加该键名,否则它会丢弃所有现有的键值,确保一个键名只有一个键值。这是它跟append()的主要区别。

values()

values()返回一个键值的迭代器,用于遍历所有键值。

1
FormData.values()

下面是用法示例。

1
2
3
4
5
6
7
8
9
const formData = new FormData();
formData.append("key1", "value1");
formData.append("key2", "value2");

for (const value of formData.values()) {
console.log(value);
}
// value1
// value2

Geolocation API

Geolocation API 用于获取用户的地理位置。

由于该功能涉及用户隐私,所以浏览器会提示用户,是否同意给出地理位置,用户可能会拒绝。另外,这个 API 只能在 HTTPS 环境使用。

浏览器通过navigator.geolocation属性提供该 API。

Geolocation 对象

navigator.geolocation属性返回一个 Geolocation 对象。该对象具有以下三个方法。

  • Geolocation.getCurrentPosition():返回一个 Position 对象,表示用户的当前位置。
  • Geolocation.watchPosition():指定一个监听函数,每当用户的位置发生变化,就执行该监听函数。
  • Geolocation.clearWatch():取消watchPosition()方法指定的监听函数。

Geolocation.getCurrentPosition()

Geolocation.getCurrentPosition()方法用于获取用户的位置。

1
navigator.geolocation.getCurrentPosition(success, error, options)

该方法接受三个参数。

  • success:用户同意给出位置时的回调函数,它的参数是一个 Position 对象。
  • error:用户拒绝给出位置时的回调函数,它的参数是一个 PositionError 对象。该参数可选。
  • options:参数对象,该参数可选。

Position 对象有两个属性。

  • Position.coords:返回一个 Coordinates 对象,表示当前位置的坐标。
  • Position.timestamp:返回一个对象,代表当前时间戳。

PositionError 对象主要有两个属性。

  • PositionError.code:整数,表示发生错误的原因。1表示无权限,有可能是用户拒绝授权;2表示无法获得位置,可能设备有故障;3表示超时。
  • PositionError.message:字符串,表示错误的描述。

参数对象option可以指定三个属性。

  • enableHighAccuracy:布尔值,是否返回高精度结果。如果设为true,可能导致响应时间变慢或(移动设备的)功耗增加;反之,如果设为false,设备可以更快速地响应。默认值为false
  • timeout:正整数,表示等待查询的最长时间,单位为毫秒。默认值为Infinity
  • maximumAge:正整数,表示可接受的缓存最长时间,单位为毫秒。如果设为0,表示不返回缓存值,必须查询当前的实际位置;如果设为Infinity,必须返回缓存值,不管缓存了多少时间。默认值为0

下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var options = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
};

function success(pos) {
var crd = pos.coords;

console.log(`经度:${crd.latitude}`);
console.log(`纬度:${crd.longitude}`);
console.log(`误差:${crd.accuracy} 米`);
}

function error(err) {
console.warn(`ERROR(${err.code}): ${err.message}`);
}

navigator.geolocation.getCurrentPosition(success, error, options);

Geolocation.watchPosition()

Geolocation.watchPosition()对象指定一个监听函数,每当用户的位置发生变化,就是自动执行这个函数。

1
navigator.geolocation.watchPosition(success[, error[, options]])

该方法接受三个参数。

  • success:监听成功的回调函数,该函数的参数为一个 Position 对象。
  • error:该参数可选,表示监听失败的回调函数,该函数的参数是一个 PositionError 对象。
  • options:该参数可选,表示监听的参数配置对象。

该方法返回一个整数值,表示监听函数的编号。该整数用来供Geolocation.clearWatch()方法取消监听。

下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var id;

var target = {
latitude : 0,
longitude: 0
};

var options = {
enableHighAccuracy: false,
timeout: 5000,
maximumAge: 0
};

function success(pos) {
var crd = pos.coords;

if (target.latitude === crd.latitude && target.longitude === crd.longitude) {
console.log('恭喜,你已经到达了指定位置。');
navigator.geolocation.clearWatch(id);
}
}

function error(err) {
console.warn('ERROR(' + err.code + '): ' + err.message);
}

id = navigator.geolocation.watchPosition(success, error, options);

Geolocation.clearWatch()

Geolocation.clearWatch()方法用来取消watchPosition()方法指定的监听函数。它的参数是watchPosition()返回的监听函数的编号。

1
navigator.geolocation.clearWatch(id);

使用方法的例子见上一节。

Coordinates 对象

Coordinates 对象是地理位置的坐标接口,Position.coords属性返回的就是这个对象。

它有以下属性,全部为只读属性。

  • Coordinates.latitude:浮点数,表示纬度。
  • Coordinates.longitude:浮点数,表示经度。
  • Coordinates.altitude:浮点数,表示海拔(单位:米)。如果不可得,返回null
  • Coordinates.accuracy:浮点数,表示经度和纬度的精度(单位:米)。
  • Coordinates.altitudeAccuracy:浮点数,表示海拔的精度(单位:米)。返回null
  • Coordinates.speed:浮点数,表示设备的速度(单位:米/秒)。如果不可得,返回null
  • Coordinates.heading:浮点数,表示设备前进的方向(单位:度)。方向按照顺时针,北方是0度,东方是90度,西方是270度。如果Coordinates.speed为0,heading属性返回NaN。如果设备无法提供方向信息,该属性返回null

下面是一个例子。

1
2
3
4
5
6
navigator.geolocation.getCurrentPosition( function (position) {
let lat = position.coords.latitude;
let long = position.coords.longitude;
console.log(`纬度:${lat.toFixed(2)}`);
console.log(`经度:${long.toFixed(2)}`);
});

参考链接

Headers 对象

简介

Headers 代表 HTTP 消息的数据头。

它通过Headers()构造方法,生成实例对象。Request.headers属性和Response.headers属性,指向的都是 Headers 实例对象。

Headers 实例对象内部,以键值对的形式保存 HTTP 消息头,可以用for...of循环进行遍历,比如for (const p of myHeaders)。新建的 Headers 实例对象,内部是空的,需要用append()方法添加键值对。

构造函数

Headers()构造函数用来新建 Headers 实例对象。

1
const myHeaders = new Headers();

它可以接受一个表示 HTTP 数据头的对象,或者另一个 Headers 实例对象,作为参数。

1
2
3
4
5
const httpHeaders = {
"Content-Type": "image/jpeg",
"X-My-Custom-Header": "Zeke are cool",
};
const myHeaders = new Headers(httpHeaders);

最后,它还可以接受一个键值对数组,作为参数。

1
2
3
4
5
const headers = [
["Set-Cookie", "greeting=hello"],
["Set-Cookie", "name=world"],
];
const myHeaders = new Headers(headers);

实例方法

append()

append()方法用来添加字段。如果字段已经存在,它会将新的值添加到原有值的末端。

它接受两个参数,第一个是字段名,第二个是字段值。它没有返回值。

1
append(name, value)

下面是用法示例。

1
2
const myHeaders = new Headers();
myHeaders.append("Content-Type", "image/jpeg");

下面是同名字段已经存在的情况。

1
2
3
myHeaders.append("Accept-Encoding", "deflate");
myHeaders.append("Accept-Encoding", "gzip");
myHeaders.get("Accept-Encoding"); // 'deflate, gzip'

上面示例中,Accept-Encoding字段已经存在,所以append()会将新的值添加到原有值的末尾。

delete()

delete()用来删除一个键值对,参数name指定删除的字段名。

1
delete(name)

如果参数name不是合法的字段名,或者是不可删除的字段,上面的命令会抛错。

下面是用法示例。

1
2
3
const myHeaders = new Headers();
myHeaders.append("Content-Type", "image/jpeg");
myHeaders.delete("Content-Type");

entries()

entries()方法用来遍历所有键值对,返回一个 iterator 指针,供for...of循环使用。

1
2
3
4
5
6
7
const myHeaders = new Headers();
myHeaders.append("Content-Type", "text/xml");
myHeaders.append("Vary", "Accept-Language");

for (const pair of myHeaders.entries()) {
console.log(`${pair[0]}: ${pair[1]}`);
}

forEach()

forEach()方法用来遍历所有键值对,对每个指定键值对执行一个指定函数。

它的第一个参数是回调函数callbackFn,第二个参数thisArgcallbackFn所用的 this 对象。

1
2
forEach(callbackFn)
forEach(callbackFn, thisArg)

回调函数callback会接受到以下参数。

  • value:当前的字段值。
  • key:当前的字段名。
  • object:当前正在执行的 Headers 对象。

下面是用法示例。

1
2
3
4
5
6
7
8
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Cookie", "This is a demo cookie");
myHeaders.append("compression", "gzip");

myHeaders.forEach((value, key) => {
console.log(`${key} ==> ${value}`);
});

get()

get()方法用于取出指定字段的字段值,它的参数就是字段名。如果字段名不合法(比如包含中文字符),它会抛错;如果字段在当前 Headers 对象不存在,它返回null

1
get(name)

下面是用法示例。

1
2
myHeaders.append("Content-Type", "image/jpeg");
myHeaders.get("Content-Type"); // "image/jpeg"

如果当前字段有多个值,get()会返回所有值。

getSetCookie()

getSetCookie()返回一个数组,包含所有Set-Cookie设定的 Cookie 值。

1
2
3
4
5
6
7
8
const headers = new Headers({
"Set-Cookie": "name1=value1",
});

headers.append("Set-Cookie", "name2=value2");

headers.getSetCookie();
// ["name1=value1", "name2=value2"]

has()

has()返回一个布尔值,表示 Headers 对象是否包含指定字段。

1
has(name)

如果参数name不是有效的 HTTP 数据头的字段名,该方法会报错。

下面是用法示例。

1
2
3
myHeaders.append("Content-Type", "image/jpeg");
myHeaders.has("Content-Type"); // true
myHeaders.has("Accept-Encoding"); // false

keys()

keys()方法用来遍历 Headers 数据头的所有字段名。它返回的是一个 iterator 对象,供for...of使用。

1
2
3
4
5
6
7
const myHeaders = new Headers();
myHeaders.append("Content-Type", "text/xml");
myHeaders.append("Vary", "Accept-Language");

for (const key of myHeaders.keys()) {
console.log(key);
}

set()

set()方法用来为指定字段添加字段值。如果字段不存在,就添加该字段;如果字段已存在,就用新的值替换老的值,这是它与append()方法的主要区别。

它的第一个参数name是字段名,第二个参数value是字段值。

1
set(name, value)

下面是用法示例。

1
2
3
4
const myHeaders = new Headers();
myHeaders.set("Accept-Encoding", "deflate");
myHeaders.set("Accept-Encoding", "gzip");
myHeaders.get("Accept-Encoding"); // 'gzip'

上面示例中,连续两次使用set()Accept-Encoding赋值,第二个值会覆盖第一个值。

values()

values()方法用来遍历 Headers 对象的字段值。它返回一个 iterator 对象,供for...of使用。

1
2
3
4
5
6
7
const myHeaders = new Headers();
myHeaders.append("Content-Type", "text/xml");
myHeaders.append("Vary", "Accept-Language");

for (const value of myHeaders.values()) {
console.log(value);
}

IntersectionObserver

网页开发时,常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。

上图的绿色方块不断滚动,顶部会提示它的可见性。

传统的实现方法是,监听到scroll事件后,调用目标元素(绿色方块)的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件密集发生,计算量很大,容易造成性能问题

IntersectionObserver API,可以自动“观察”元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做“交叉观察器”(intersection oberserver)。

简介

IntersectionObserver API 的用法,简单来说就是两行。

1
2
var observer = new IntersectionObserver(callback, options);
observer.observe(target);

上面代码中,IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(该参数可选)。

IntersectionObserver()的返回值是一个观察器实例。实例的observe()方法可以指定观察哪个 DOM 节点。

1
2
3
4
5
6
7
8
// 开始观察
observer.observe(document.getElementById('example'));

// 停止观察
observer.unobserve(element);

// 关闭观察器
observer.disconnect();

上面代码中,observe()的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。

1
2
observer.observe(elementA);
observer.observe(elementB);

注意,IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。规格写明,IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。

IntersectionObserver.observe()

IntersectionObserver 实例的observe()方法用来启动对一个 DOM 元素的观察。该方法接受两个参数:回调函数callback和配置对象options

callback 参数

目标元素的可见性变化时,就会调用观察器的回调函数callback

callback会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

1
2
3
4
5
var observer = new IntersectionObserver(
(entries, observer) => {
console.log(entries);
}
);

上面代码中,回调函数采用的是箭头函数的写法。callback函数的参数(entries)是一个数组,每个成员都是一个IntersectionObserverEntry对象(详见下文)。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。

IntersectionObserverEntry 对象

IntersectionObserverEntry对象提供目标元素的信息,一共有六个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
time: 3893.92,
rootBounds: ClientRect {
bottom: 920,
height: 1024,
left: 0,
right: 1024,
top: 0,
width: 920
},
boundingClientRect: ClientRect {
// ...
},
intersectionRect: ClientRect {
// ...
},
intersectionRatio: 0.54,
target: element
}

每个属性的含义如下。

  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • target:被观察的目标元素,是一个 DOM 节点对象
  • rootBounds:容器元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有容器元素(即直接相对于视口滚动),则返回null
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视口(或容器元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例,即intersectionRectboundingClientRect的比例,完全可见时为1,完全不可见时小于等于0

上图中,灰色的水平方框代表视口,深红色的区域代表四个被观察的目标元素。它们各自的intersectionRatio图中都已经注明。

我写了一个 Demo,演示IntersectionObserverEntry对象。注意,这个 Demo 只能在 Chrome 51+ 运行。

Option 对象

IntersectionObserver构造函数的第二个参数是一个配置对象。它可以设置以下属性。

(1)threshold 属性

threshold属性决定了什么时候触发回调函数,即元素进入视口(或者容器元素)多少比例时,执行回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。

如果threshold属性是0.5,当元素进入视口50%时,触发回调函数。如果值为[0.3, 0.6],则当元素进入30%和60%是触发回调函数。

1
2
3
4
5
6
new IntersectionObserver(
entries => {/* … */},
{
threshold: [0, 0.25, 0.5, 0.75, 1]
}
);

用户可以自定义这个数组。比如,上例的[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。

(2)root 属性,rootMargin 属性

IntersectionObserver不仅可以观察元素相对于视口的可见性,还可以观察元素相对于其所在容器的可见性。容器内滚动也会影响目标元素的可见性,参见本文开始时的那张示意图。

IntersectionObserver API 支持容器内滚动。root属性指定目标元素所在的容器节点。注意,容器元素必须是目标元素的祖先节点。

1
2
3
4
5
6
7
8
9
var opts = {
root: document.querySelector('.container'),
rootMargin: '0px 0px -200px 0px'
};

var observer = new IntersectionObserver(
callback,
opts
);

上面代码中,除了root属性,还有rootMargin属性。该属性用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。它的写法类似于 CSS 的margin属性,比如0px 0px 0px 0px,依次表示 top、right、bottom 和 left 四个方向的值。

上例的0px 0px -200px 0px,表示容器的下边缘向上收缩200像素,导致页面向下滚动时,目标元素的顶部进入可视区域200像素以后,才会触发回调函数。

这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器。

实例

惰性加载(lazy load)

有时,我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。这就叫做“惰性加载”。

有了 IntersectionObserver API,实现起来就很容易了。图像的 HTML 代码可以写成下面这样。

1
2
3
<img src="placeholder.png" data-src="img-1.jpg">
<img src="placeholder.png" data-src="img-2.jpg">
<img src="placeholder.png" data-src="img-3.jpg">

上面代码中,图像默认显示一个占位符,data-src属性是惰性加载的真正图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function query(selector) {
return Array.from(document.querySelectorAll(selector));
}

var observer = new IntersectionObserver(
function(entries) {
entries.forEach(function(entry) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
});
}
);

query('.lazy-loaded').forEach(function (item) {
observer.observe(item);
});

上面代码中,只有图像开始可见时,才会加载真正的图像文件。

无限滚动

无限滚动(infinite scroll)指的是,随着网页滚动到底部,不断加载新的内容到页面,它的实现也很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
var intersectionObserver = new IntersectionObserver(
function (entries) {
// 如果不可见,就返回
if (entries[0].intersectionRatio <= 0) return;
loadItems(10);
console.log('Loaded new items');
}
);

// 开始观察
intersectionObserver.observe(
document.querySelector('.scrollerFooter')
);

无限滚动时,最好像上例那样,页面底部有一个页尾栏(又称sentinels,上例是.scrollerFooter)。一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。否则就需要每一次页面加入新内容时,都调用observe()方法,对新增内容的底部建立观察。

视频自动播放

下面是一个视频元素,希望它完全进入视口的时候自动播放,离开视口的时候自动暂停。

1
<video src="foo.mp4" controls=""></video>

下面是 JS 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let video = document.querySelector('video');
let isPaused = false;

let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.intersectionRatio != 1 && !video.paused) {
video.pause();
isPaused = true;
} else if (isPaused) {
video.play();
isPaused=false;
}
});
}, {threshold: 1});

observer.observe(video);

上面代码中,IntersectionObserver()的第二个参数是配置对象,它的threshold属性等于1,即目标元素完全可见时触发回调函数。

参考链接

Intl.RelativeTimeFormat

很多日期库支持显示相对时间,比如“昨天”、“五分钟前”、“两个月之前”等等。由于不同的语言,日期显示的格式和相关词语都不同,造成这些库的体积非常大。

现在,浏览器提供内置的 Intl.RelativeTimeFormat API,可以不使用这些库,直接显示相对时间。

基本用法

Intl.RelativeTimeFormat()是一个构造函数,接受一个语言代码作为参数,返回一个相对时间的实例对象。如果省略参数,则默认传入当前运行时的语言代码。

1
2
3
4
5
6
7
8
9
10
const rtf = new Intl.RelativeTimeFormat('en');

rtf.format(3.14, 'second') // "in 3.14 seconds"
rtf.format(-15, 'minute') // "15 minutes ago"
rtf.format(8, 'hour') // "in 8 hours"
rtf.format(-2, 'day') // "2 days ago"
rtf.format(3, 'week') // "in 3 weeks"
rtf.format(-5, 'month') // "5 months ago"
rtf.format(2, 'quarter') // "in 2 quarters"
rtf.format(-42, 'year') // "42 years ago"

上面代码指定使用英语显示相对时间。

下面是使用西班牙语显示相对时间的例子。

1
2
3
4
5
6
7
8
9
10
const rtf = new Intl.RelativeTimeFormat('es');

rtf.format(3.14, 'second') // "dentro de 3,14 segundos"
rtf.format(-15, 'minute') // "hace 15 minutos"
rtf.format(8, 'hour') // "dentro de 8 horas"
rtf.format(-2, 'day') // "hace 2 días"
rtf.format(3, 'week') // "dentro de 3 semanas"
rtf.format(-5, 'month') // "hace 5 meses"
rtf.format(2, 'quarter') // "dentro de 2 trimestres"
rtf.format(-42, 'year') // "hace 42 años"

Intl.RelativeTimeFormat()还可以接受一个配置对象,作为第二个参数,用来精确指定相对时间实例的行为。配置对象共有下面这些属性。

  • options.style:表示返回字符串的风格,可能的值有long(默认值,比如“in 1 month”)、short(比如“in 1 mo.”)、narrow(比如“in 1 mo.”)。对于一部分语言来说,narrow风格和short风格是类似的。
  • options.localeMatcher:表示匹配语言参数的算法,可能的值有best fit(默认值)和lookup
  • options.numeric:表示返回字符串是数字显示,还是文字显示,可能的值有always(默认值,总是文字显示)和auto(自动转换)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 下面的配置对象,传入的都是默认值
const rtf = new Intl.RelativeTimeFormat('en', {
localeMatcher: 'best fit', // 其他值:'lookup'
style: 'long', // 其他值:'short' or 'narrow'
numeric: 'always', // 其他值:'auto'
});

// Now, let’s try some special cases!

rtf.format(-1, 'day') // "1 day ago"
rtf.format(0, 'day') // "in 0 days"
rtf.format(1, 'day') // "in 1 day"
rtf.format(-1, 'week') // "1 week ago"
rtf.format(0, 'week') // "in 0 weeks"
rtf.format(1, 'week') // "in 1 week"

上面代码中,显示的是“1 day ago”,而不是“yesterday”;显示的是“in 0 weeks”,而不是“this week”。这是因为默认情况下,相对时间显示的是数值形式,而不是文字形式。

改变这个行为,可以把配置对象的numeric属性改成auto

1
2
3
4
5
6
7
8
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

rtf.format(-1, 'day') // "yesterday"
rtf.format(0, 'day') // "today"
rtf.format(1, 'day') // "tomorrow"
rtf.format(-1, 'week') // "last week"
rtf.format(0, 'week') // "this week"
rtf.format(1, 'week') // "next week"

Intl.RelativeTimeFormat.prototype.format()

相对时间实例对象的format方法,接受两个参数,依次为时间间隔的数值和单位。其中,“单位”是一个字符串,可以接受以下八个值。

  • year
  • quarter
  • month
  • week
  • day
  • hour
  • minute
  • second
1
2
3
let rtf = new Intl.RelativeTimeFormat('en');
rtf.format(-1, "day") // "yesterday"
rtf.format(2.15, "day") // "in 2.15 days

Intl.RelativeTimeFormat.prototype.formatToParts()

相对时间实例对象的formatToParts()方法的参数跟format()方法一样,但是返回的是一个数组,用来精确控制相对时间的每个部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

rtf.format(-1, 'day')
// "yesterday"
rtf.formatToParts(-1, 'day');
// [{ type: "literal", value: "yesterday" }]

rtf.format(3, 'week');
// "in 3 weeks"
rtf.formatToParts(3, 'week');
// [
// { type: 'literal', value: 'in ' },
// { type: 'integer', value: '3', unit: 'week' },
// { type: 'literal', value: ' weeks' }
// ]

返回数组的每个成员都是一个对象,拥有两个属性。

  • type:字符串,表示输出值的类型。
  • value:字符串,表示输出的内容。
  • unit:如果输出内容表示一个数值(即type属性不是literal),那么还会有unit属性,表示数值的单位。

参考链接

Intl segmenter API

简介

Intl.Segmenter 是浏览器内置的用于文本分词的 API。

使用时,先用Intl.Segmenter()新建一个分词器对象。

1
2
3
4
const segmenter = new Intl.Segmenter(
'en',
{ granularity: 'word' }
);

Intl.Segmenter()接受两个参数,第一个是所要分词的语言简称(上例是en),第二个参数是一个配置对象,有以下两个属性。

  • localeMatcher:指定分词算法,有两个可能的值,一个是lookup,表示采用特定的算法(BCP 47),另一个是best fit(默认值),表示采用操作系统或浏览器现有的尽可能适用的算法。
  • granularity:表示分词的颗粒度,有三个可能的值:grapheme(字符,这是默认值),word(词语),sentence(句子)。

拿到分词器对象以后,就可以进行分词了。

1
2
3
4
5
6
7
8
9
const segmenter = new Intl.Segmenter(
'en',
{ granularity: 'word' }
);

const segments = segmenter.segment('This has four words!');

Array.from(segments).map((segment) => segment.segment);
// ['This', ' ', 'has', ' ', 'four', ' ', 'words', '!']

上面示例中,变量segmenter是分词器对象,可以对英语进行分词,颗粒度是词语。所以,“This has four words!”被分成了8个部分,包括4个词语、3个空格和1个标点符号。

分词器对象的segment()方法是实际的分词方法,它的参数是需要分词的文本,返回值是一个具有迭代器接口的分词结果对象。Array.from()将这个分词结果对象转成数组,也可以采用[...segments]的写法。

下面的例子是过滤掉非词语字符。

1
2
3
4
5
6
const segments = segmenter.segment('This has four words!');

Array.from(segments)
.filter((segment) => segment.isWordLike)
.map((segment) => segment.segment);
// ['This', 'has', 'four', 'words']

上面示例中,Array.from()将分词结果对象转成一个数组,变量segment是数组的每个成员,它也是一个对象。该对象的isWordLike属性是一个布尔值,表示当前值是否为一个真正的词,而该对象的segment属性(上例的segment.segment)则是真正的分词结果。

Intl Segmenter 支持各种语言,下面是日语分词的例子。

1
2
3
4
5
const segmenter = new Intl.Segmenter('ja', { granularity: 'word' });
const segments = segmenter.segment('これは日本語のテキストです');

Array.from(segments).map((segment) => segment.segment);
// ['これ', 'は', '日本語', 'の', 'テキスト', 'です']

下面是法语的例子。

1
2
3
4
5
6
7
const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' });
const string1 = 'Que ma joie demeure';

const iterator1 = segmenterFr.segment(string1)[Symbol.iterator]();

iterator1.next().value.segment // 'Que'
iterator1.next().value.segment // ' '

静态方法

Intl.Segmenter.supportedLocalesOf()

Intl.Segmenter.supportedLocalesOf()返回一个数组,用来检测当前环境是否支持指定语言的分词。

1
2
3
4
5
const locales1 = ['ban', 'id-u-co-pinyin', 'de-ID'];
const options1 = { localeMatcher: 'lookup', granularity: 'string' };

Intl.Segmenter.supportedLocalesOf(locales1, options1)
// ["id-u-co-pinyin", "de-ID"]

它接受两个参数,第一个参数是一个数组,数组成员是需要检测的语言简称;第二个参数是配置对象,跟构造方法的第二个参数是一致的,可以省略。

上面示例中,需要检测的三种语言分别是巴厘岛语(ban)、印度尼西亚语(id-u-co-pinyin)、德语(de-ID)。结果显示只支持前两者,不支持巴厘岛语。

实例方法

resolvedOptions()

实例对象的resolvedOptions()方法,用于获取构造该实例时的参数。

1
2
3
4
5
const segmenter1 = new Intl.Segmenter('fr-FR');
const options1 = segmenter1.resolvedOptions();

options1.locale // "fr-FR"
options1.granularity // "grapheme"

上面示例中,resolveOptions()方法返回了一个对象,该对象的locale属性对应构造方法的第一个参数,granularity属性对应构造方法第二个参数对象的颗粒度属性。

segment()

实例对象的segment()方法进行实际的分词。

1
2
3
4
5
6
7
const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' });
const string1 = 'Que ma joie demeure';

const segments = segmenterFr.segment(string1);

segments.containing(5)
// {segment: 'ma', index: 4, input: 'Que ma joie demeure', isWordLike: true}

segment()方法的返回结果是一个具有迭代器接口的分词结果对象,有三种方法进行处理。

(1)使用Array.from()或扩展运算符(...)将分词结果对象转成数组。

1
2
3
4
5
6
7
8
9
10
11
const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' });
const string1 = 'Que ma joie demeure';

const iterator1 = segmenterFr.segment(string1);

Array.from(iterator1).map(segment => {
if (segment.segment.length > 4) {
console.log(segment.segment);
}
})
// demeure

上面示例中,segmenterFr.segment()返回一个针对string1的分词结果对象,该对象具有迭代器接口。Array.from()将其转为数组,数组的每个成员是一个分词颗粒对象,该对象的segment属性就是分词结果。分词颗粒对象的介绍,详见后文。

(2)使用for...of循环,遍历分词结果对象。

1
2
3
4
5
6
7
8
9
10
11
const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' });
const string1 = 'Que ma joie demeure';

const iterator1 = segmenterFr.segment(string1);

for (const segment of iterator1) {
if (segment.segment.length > 4) {
console.log(segment.segment);
}
}
// demeure

上面示例中,for...of默认调用分词结果对象的迭代器接口,获取每一轮的分词颗粒对象。

由于迭代器接口是在Symbol.iterator属性上面,所以实际执行的代码如下。

1
2
3
4
5
6
7
8
9
10
11
const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' });
const string1 = 'Que ma joie demeure';

const iterator1 = segmenterFr.segment(string1)[Symbol.iterator]();

for (const segment of iterator1) {
if (segment.segment.length > 4) {
console.log(segment.segment);
}
}
// "demeure"

for...of循环每一轮得到的是一个分词颗粒对象,该对象的segment属性就是当前的分词结果,详见下文。

(3)使用containing()方法获取某个位置的分词颗粒对象。

1
2
3
4
5
6
7
const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' });
const string1 = 'Que ma joie demeure';

const segments = segmenterFr.segment(string1);

segments.containing(5)
// {segment: 'ma', index: 4, input: 'Que ma joie demeure', isWordLike: true}

containing()方法的参数是一个整数,表示原始字符串的指定位置(从0开始计算)。如果省略该参数,则默认为0。

containing()的返回值是该位置的分词颗粒对象,如果参数位置超出原始字符串,则返回undefined。分词颗粒对象有以下属性。

  • segment:指定位置对应的分词结果。
  • index:本次分词在原始字符串的开始位置(从0开始)。
  • input:进行分词的原始字符串。
  • isWordLike:如果分词颗粒度为word,该属性返回一个布尔值,表示当前值是否一个真正的词。如果分词颗粒度不为word,则返回undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const input = "Allons-y!";

const segmenter = new Intl.Segmenter("fr", { granularity: "word" });
const segments = segmenter.segment(input);

let current = segments.containing();
// { index: 0, segment: "Allons", isWordLike: true }

current = segments.containing(4);
// { index: 0, segment: "Allons", isWordLike: true }

current = segments.containing(6);
// { index: 6, segment: "-", isWordLike: false }

current = segments.containing(current.index + current.segment.length);
// { index: 7, segment: "y", isWordLike: true }

current = segments.containing(current.index + current.segment.length);
// { index: 8, segment: "!", isWordLike: false }

current = segments.containing(current.index + current.segment.length);
// undefined

上面示例中,分词结果中除了空格和标点符号,其他情况下,isWordLike都返回false

Page Lifecycle API

Android、iOS 和最新的 Windows 系统可以随时自主地停止后台进程,及时释放系统资源。也就是说,网页可能随时被系统丢弃掉。以前的浏览器 API 完全没有考虑到这种情况,导致开发者根本没有办法监听到系统丢弃页面。

为了解决这个问题,W3C 新制定了一个 Page Lifecycle API,统一了网页从诞生到卸载的行为模式,并且定义了新的事件,允许开发者响应网页状态的各种转换。

有了这个 API,开发者就可以预测网页下一步的状态,从而进行各种针对性的处理。Chrome 68 支持这个 API,对于老式浏览器可以使用谷歌开发的兼容库 PageLifecycle.js

生命周期阶段

网页的生命周期分成六个阶段,每个时刻只可能处于其中一个阶段。

(1)Active 阶段

在 Active 阶段,网页处于可见状态,且拥有输入焦点。

(2)Passive 阶段

在 Passive 阶段,网页可见,但没有输入焦点,无法接受输入。UI 更新(比如动画)仍然在执行。该阶段只可能发生在桌面同时有多个窗口的情况。

(3)Hidden 阶段

在 Hidden 阶段,用户的桌面被其他窗口占据,网页不可见,但尚未冻结。UI 更新不再执行。

(4)Terminated 阶段

在 Terminated 阶段,由于用户主动关闭窗口,或者在同一个窗口前往其他页面,导致当前页面开始被浏览器卸载并从内存中清除。注意,这个阶段总是在 Hidden 阶段之后发生,也就是说,用户主动离开当前页面,总是先进入 Hidden 阶段,再进入 Terminated 阶段。

这个阶段会导致网页卸载,任何新任务都不会在这个阶段启动,并且如果运行时间太长,正在进行的任务可能会被终止。

(5)Frozen 阶段

如果网页处于 Hidden 阶段的时间过久,用户又不关闭网页,浏览器就有可能冻结网页,使其进入 Frozen 阶段。不过,也有可能,处于可见状态的页面长时间没有操作,也会进入 Frozen 阶段。

这个阶段的特征是,网页不会再被分配 CPU 计算资源。定时器、回调函数、网络请求、DOM 操作都不会执行,不过正在运行的任务会执行完。浏览器可能会允许 Frozen 阶段的页面,周期性复苏一小段时间,短暂变回 Hidden 状态,允许一小部分任务执行。

(6)Discarded 阶段

如果网页长时间处于 Frozen 阶段,用户又不唤醒页面,那么就会进入 Discarded 阶段,即浏览器自动卸载网页,清除该网页的内存占用。不过,Passive 阶段的网页如果长时间没有互动,也可能直接进入 Discarded 阶段。

这一般是在用户没有介入的情况下,由系统强制执行。任何类型的新任务或 JavaScript 代码,都不能在此阶段执行,因为这时通常处在资源限制的状况下。

网页被浏览器自动 Discarded 以后,它的 Tab 窗口还是在的。如果用户重新访问这个 Tab 页,浏览器将会重新向服务器发出请求,再一次重新加载网页,回到 Active 阶段。

常见场景

以下是几个常见场景的网页生命周期变化。

(1)用户打开网页后,又切换到其他 App,但只过了一会又回到网页。

网页由 Active 变成 Hidden,又变回 Active。

(2)用户打开网页后,又切换到其他 App,并且长时候使用后者,导致系统自动丢弃网页。

网页由 Active 变成 Hidden,再变成 Frozen,最后 Discarded。

(3)用户打开网页后,又切换到其他 App,然后从任务管理器里面将浏览器进程清除。

网页由 Active 变成 Hidden,然后 Terminated。

(4)系统丢弃了某个 Tab 里面的页面后,用户重新打开这个 Tab。

网页由 Discarded 变成 Active。

事件

生命周期的各个阶段都有自己的事件,以供开发者指定监听函数。这些事件里面,只有两个是新定义的(freeze事件和resume事件),其它都是现有的。

注意,网页的生命周期事件是在所有帧(frame)触发,不管是底层的帧,还是内嵌的帧。也就是说,内嵌的<iframe>网页跟顶层网页一样,都会同时监听到下面的事件。

focus 事件

focus事件在页面获得输入焦点时触发,比如网页从 Passive 阶段变为 Active 阶段。

blur 事件

blur事件在页面失去输入焦点时触发,比如网页从 Active 阶段变为 Passive 阶段。

visibilitychange 事件

visibilitychange事件在网页可见状态发生变化时触发,一般发生在以下几种场景。

  • 用户隐藏页面(切换 Tab、最小化浏览器),页面由 Active 阶段变成 Hidden 阶段。
  • 用户重新访问隐藏的页面,页面由 Hidden 阶段变成 Active 阶段。
  • 用户关闭页面,页面会先进入 Hidden 阶段,然后进入 Terminated 阶段。

可以通过document.onvisibilitychange属性指定这个事件的回调函数。

freeze 事件

freeze事件在网页进入 Frozen 阶段时触发。

可以通过document.onfreeze属性指定在进入 Frozen 阶段时调用的回调函数。

1
2
3
4
5
6
7
function handleFreeze(e) {
// Handle transition to FROZEN
}
document.addEventListener('freeze', handleFreeze);

# 或者
document.onfreeze = function() { … }

这个事件的监听函数,最长只能运行500毫秒。并且只能复用已经打开的网络连接,不能发起新的网络请求。

注意,从 Frozen 阶段进入 Discarded 阶段,不会触发任何事件,无法指定回调函数,只能在进入 Frozen 阶段时指定回调函数。

resume 事件

resume事件在网页离开 Frozen 阶段,变为 Active / Passive / Hidden 阶段时触发。

document.onresume属性指的是页面离开 Frozen 阶段、进入可用状态时调用的回调函数。

1
2
3
4
5
6
7
function handleResume(e) {
// handle state transition FROZEN -> ACTIVE
}
document.addEventListener("resume", handleResume);

# 或者
document.onresume = function() { … }

pageshow 事件

pageshow事件在用户加载网页时触发。这时,有可能是全新的页面加载,也可能是从缓存中获取的页面。如果是从缓存中获取,则该事件对象的event.persisted属性为true,否则为false

这个事件的名字有点误导,它跟页面的可见性其实毫无关系,只跟浏览器的 History 记录的变化有关。

pagehide 事件

pagehide事件在用户离开当前网页、进入另一个网页时触发。它的前提是浏览器的 History 记录必须发生变化,跟网页是否可见无关。

如果浏览器能够将当前页面添加到缓存以供稍后重用,则事件对象的event.persisted属性为true。 如果为true。如果页面添加到了缓存,则页面进入 Frozen 状态,否则进入 Terminatied 状态。

beforeunload 事件

beforeunload事件在窗口或文档即将卸载时触发。该事件发生时,文档仍然可见,此时卸载仍可取消。经过这个事件,网页进入 Terminated 状态。

unload 事件

unload事件在页面正在卸载时触发。经过这个事件,网页进入 Terminated 状态。

获取当前阶段

如果网页处于 Active、Passive 或 Hidden 阶段,可以通过下面的代码,获得网页当前的状态。

1
2
3
4
5
6
7
8
9
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};

如果网页处于 Frozen 和 Terminated 状态,由于定时器代码不会执行,只能通过事件监听判断状态。进入 Frozen 阶段,可以监听freeze事件;进入 Terminated 阶段,可以监听pagehide事件。

document.wasDiscarded

如果某个选项卡处于 Frozen 阶段,就随时有可能被系统丢弃,进入 Discarded 阶段。如果后来用户再次点击该选项卡,浏览器会重新加载该页面。

这时,开发者可以通过判断document.wasDiscarded属性,了解先前的网页是否被丢弃了。

1
2
3
4
5
if (document.wasDiscarded) {
// 该网页已经不是原来的状态了,曾经被浏览器丢弃过
// 恢复以前的状态
getPersistedState(self.discardedClientId);
}

同时,window对象上会新增window.clientIdwindow.discardedClientId两个属性,用来恢复丢弃前的状态。

参考链接

Page Visibility API

简介

有时候,开发者需要知道,用户正在离开页面。常用的方法是监听下面三个事件。

  • pagehide
  • beforeunload
  • unload

但是,这些事件在手机上可能不会触发,页面就直接关闭了。因为手机系统可以将一个进程直接转入后台,然后杀死。

  • 用户点击了一条系统通知,切换到另一个 App。
  • 用户进入任务切换窗口,切换到另一个 App。
  • 用户点击了 Home 按钮,切换回主屏幕。
  • 操作系统自动切换到另一个 App(比如,收到一个电话)。

上面这些情况,都会导致手机将浏览器进程切换到后台,然后为了节省资源,可能就会杀死浏览器进程。

以前,页面被系统切换,以及系统清除浏览器进程,是无法监听到的。开发者想要指定,任何一种页面卸载情况下都会执行的代码,也是无法做到的。为了解决这个问题,就诞生了 Page Visibility API。不管手机或桌面电脑,所有情况下,这个 API 都会监听到页面的可见性发生变化。

这个新的 API 的意义在于,通过监听网页的可见性,可以预判网页的卸载,还可以用来节省资源,减缓电能的消耗。比如,一旦用户不看网页,下面这些网页行为都是可以暂停的。

  • 对服务器的轮询
  • 网页动画
  • 正在播放的音频或视频

document.visibilityState

这个 API 主要在document对象上,新增了一个document.visibilityState属性。该属性返回一个字符串,表示页面当前的可见性状态,共有三个可能的值。

  • hidden:页面彻底不可见。
  • visible:页面至少一部分可见。
  • prerender:页面即将或正在渲染,处于不可见状态。

其中,hidden状态和visible状态是所有浏览器都必须支持的。prerender状态只在支持“预渲染”的浏览器上才会出现,比如 Chrome 浏览器就有预渲染功能,可以在用户不可见的状态下,预先把页面渲染出来,等到用户要浏览的时候,直接展示渲染好的网页。

只要页面可见,哪怕只露出一个角,document.visibilityState属性就返回visible。只有以下四种情况,才会返回hidden

  • 浏览器最小化。
  • 浏览器没有最小化,但是当前页面切换成了背景页。
  • 浏览器将要卸载(unload)页面。
  • 操作系统触发锁屏屏幕。

可以看到,上面四种场景涵盖了页面可能被卸载的所有情况。也就是说,页面卸载之前,document.visibilityState属性一定会变成hidden。事实上,这也是设计这个 API 的主要目的。

另外,早期版本的 API,这个属性还有第四个值unloaded,表示页面即将卸载,现在已经被废弃了。

注意,document.visibilityState属性只针对顶层窗口,内嵌的<iframe>页面的document.visibilityState属性由顶层窗口决定。使用 CSS 属性隐藏<iframe>页面(比如display: none;),并不会影响内嵌页面的可见性。

document.hidden

由于历史原因,这个 API 还定义了document.hidden属性。该属性只读,返回一个布尔值,表示当前页面是否可见。

document.visibilityState属性返回visible时,document.hidden属性返回false;其他情况下,都返回true

该属性只是出于历史原因而保留的,只要有可能,都应该使用document.visibilityState属性,而不是使用这个属性。

visibilitychange 事件

只要document.visibilityState属性发生变化,就会触发visibilitychange事件。因此,可以通过监听这个事件(通过document.addEventListener()方法或document.onvisibilitychange属性),跟踪页面可见性的变化。

1
2
3
4
5
6
7
8
9
10
11
document.addEventListener('visibilitychange', function () {
// 用户离开了当前页面
if (document.visibilityState === 'hidden') {
document.title = '页面不可见';
}

// 用户打开或回到页面
if (document.visibilityState === 'visible') {
document.title = '页面可见';
}
});

上面代码是 Page Visibility API 的最基本用法,可以监听可见性变化。

下面是另一个例子,一旦页面不可见,就暂停视频播放。

1
2
3
4
5
6
7
8
9
10
var vidElem = document.getElementById('video-demo');
document.addEventListener('visibilitychange', startStopVideo);

function startStopVideo() {
if (document.visibilityState === 'hidden') {
vidElem.pause();
} else if (document.visibilityState === 'visible') {
vidElem.play();
}
}

页面卸载

下面专门讨论一下,如何正确监听页面卸载。

页面卸载可以分成三种情况。

  • 页面可见时,用户关闭 Tab 页或浏览器窗口。
  • 页面可见时,用户在当前窗口前往另一个页面。
  • 页面不可见时,用户或系统关闭浏览器窗口。

这三种情况,都会触发visibilitychange事件。前两种情况,该事件在用户离开页面时触发;最后一种情况,该事件在页面从可见状态变为不可见状态时触发。

由此可见,visibilitychange事件比pagehidebeforeunloadunload事件更可靠,所有情况下都会触发(从visible变为hidden)。因此,可以只监听这个事件,运行页面卸载时需要运行的代码,不用监听后面那三个事件。

甚至可以这样说,unload事件在任何情况下都不必监听,beforeunload事件只有一种适用场景,就是用户修改了表单,没有提交就离开当前页面。另一方面,指定了这两个事件的监听函数,浏览器就不会缓存当前页面。

参考链接

Request API

浏览器原生提供 Request() 构造函数,用来构造发给服务器的 HTTP 请求。它生成的 Response 实例,可以作为fetch()的参数。

注意,构造一个 Request 对象,只是构造出一个数据结构,本身并不会发出 HTTP 请求,只有将它传入fetch()方法才会真的发出请求。

构造方法

Request 作为构造函数的语法如下,返回一个 Request 实例对象。

1
new Request(url: String, [init: Object]): Request

它的第一个参数是请求的 URL 字符串,第二个参数是一个可选的配置对象,用来构造 HTTP 请求,该对象的类型描述如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
body: Object
cache: String
credentials: String
headers: Object
integrity: String
keepalive: Boolean
method: String
mode: String
redirect: String
referrer: String
referrerPolicy: String
requestMode: String
requestCredentials: String
signal: AbortSignal
}

第二个参数配置对象的各个属性的含义如下。

  • body:HTTP 请求的数据体,必须是 Blob、BufferSource、FormData、String、URLSearchParams 类型之一。
  • cache:请求的缓存模式。
  • credentials:请求所用的凭证,可以设为 omit、same-origini、include。Chrome 47 之前,默认值为 same-origin;Chrome 47 之后,默认值为 include。
  • headers:一个代表 HTTP 请求数据头的对象,类型为 Headers 对象实例。
  • integrity:请求的资源的资源完整度验证值,比如sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=
  • method:HTTP 方法,一般为GETPOSTDELETE,默认是GET
  • mode:请求模式,比如 cors、no-cors、navigate,默认为 cors。
  • redirect:请求所用的模式,可以设为 error、follow、manual,默认为 follow。
  • referrer:请求的来源,默认为 about:client。

下面是两个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 示例一
const request = new Request('flowers.jpg');

// 示例二
const myInit = {
method: "GET",
headers: {
"Content-Type": "image/jpeg",
},
mode: "cors",
cache: "default",
};

const request = new Request('flowers.jpg', myInit);

Request()还有另一种语法,第一个参数是另一个 Request 对象,第二个参数还是一个配置对象。它返回一个新的 Request 对象,相当于对第一个参数 Request 对象进行修改。

1
new Request(request: Request, [init: Object]): Request

实例属性

Request 实例对象的属性,大部分就是它的构造函数第二个参数配置对象的属性。

(1)body

body属性返回 HTTP 请求的数据体,它的值是一个 ReadableStream 对象或 null(GETHEAD请求时没有数据体)。

1
2
3
4
5
6
const request = new Request('/myEndpoint', {
method: "POST",
body: "Hello world",
});

request.body; // ReadableStream 对象

注意,Firefox 不支持该属性。

(2)bodyused

bodyUsed属性是一个布尔值,表示body是否已经被读取了。

(3)cache

cache属性是一个只读字符串,表示请求的缓存模式,可能的值有 default、force-cache、no-cache、no-store、only-if-cached、reload。

(4)credentials

credentials属性是一个只读字符串,表示跨域请求时是否携带其他域的 cookie。可能的值有 omit(不携带)、 include(携带)、same-origin(只携带同源 cookie)。

(5)destination

destination属性是一个字符串,表示请求内容的类型,可能的值有 ‘’、’audio’、’audioworklet’、’document’、’embed’、’font’、’frame’、’iframe’、’image’、’manifest’、’object’、’paintworklet’、 ‘report’、’script’、’sharedworker’、’style’、’track’、’video’、’worker’、’xslt’ 等。

(6)headers

headers属性是一个只读的 Headers 实例对象,表示请求的数据头。

(7)integrity

integrity属性表示所请求资源的完整度的验证值。

(8)method

method属性是一个只读字符串,表示请求的方法(GET、POST 等)。

(9)mode

mode属性是一个只读字符串,用来验证是否可以有效地发出跨域请求,可能的值有 same-origin、no-cors、cors。

(10)redirect

redirect属性是一个只读字符串,表示重定向时的处理模式,可能的值有 follow、error、manual。

(11)referrer

referrer属性是一个只读字符串,表示请求的引荐 URL。

(12)referrerPolicy

referrerPolicy属性是一个只读字符串,决定了referrer属性是否要包含在请求里面的处理政策。

(13)signal

signal是一个只读属性,包含与当前请求相对应的中断信号 AbortSignal 对象。

(14)url

url是一个只读字符串,包含了当前请求的字符串。

1
2
const myRequest = new Request('flowers.jpg');
const myURL = myRequest.url;

实例方法

取出数据体的方法

  • arrayBuffer():返回一个 Promise 对象,将 Request 的数据体作为 ArrayBuffer 对象返回。
  • blob():返回一个 Promise 对象,将 Request 的数据体作为 Blob 对象返回。
  • json():返回一个 Promise 对象,将 Request 的数据体作为 JSON 对象返回。
  • text():返回一个 Promise 对象,将 Request 的数据体作为字符串返回。
  • formData():返回一个 Promise 对象,将 Request 的数据体作为表单数据 FormData 对象返回。

下面是json()方法的一个示例。

1
2
3
4
5
6
7
8
9
10
const obj = { hello: "world" };

const request = new Request("/myEndpoint", {
method: "POST",
body: JSON.stringify(obj),
});

request.json().then((data) => {
// 处理 JSON 数据
});

.formData()方法返回一个 Promise 对象,最终得到的是一个 FormData 表单对象,里面是用键值对表示的各种表单元素。该方法很少使用,因为需要拦截发给服务器的请求的场景不多,一般用在 Service Worker 拦截和处理网络请求,以修改表单数据,然后再发送到服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
self.addEventListener('fetch', event => {
// 拦截表单提交请求
if (
event.request.method === 'POST' &&
event.request.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
) {
event.respondWith(handleFormSubmission(event.request));
}
});

async function handleFormSubmission(request) {
const formData = await request.formData();
formData.append('extra-field', 'extra-value');

const newRequest = new Request(request.url, {
method: request.method,
headers: request.headers,
body: new URLSearchParams(formData)
});

return fetch(newRequest);
}

上面示例中,Service Worker 拦截表单请求以后,添加了一个表单成员,再调用fetch()向服务器发出修改后的请求。

clone()

clone()用来复制 HTTP 请求对象。

1
2
const myRequest = new Request('flowers.jpg');
const newRequest = myRequest.clone();

Response API

浏览器原生提供Response()构造函数,用来构造服务器响应。

fetch()方法返回的就是一个 Response 对象。

构造方法

Response()作为构造方法调用时,返回 Response 实例。

1
2
3
4
5
6
7
// 定义
new Response([body:Object, [init : Object]]): Response

// 用法
new Response()
new Response(body)
new Response(body, options)

它带有两个参数,都是可选的。

第一个参数body代表服务器返回的数据体,必须是下面类型之一:ArrayBuffer、ArrayBufferView、Blob、FormData、ReadableStream、String、URLSearchParams。

第二个参数init是一个对象,代表服务器返回的数据头,类型描述如下。

1
2
3
4
5
{
status: Number
statusText: String
headers: Object
}

下面是一个例子。

1
2
3
const myBlob = new Blob();
const myOptions = { status: 200, statusText: "OK" };
const myResponse = new Response(myBlob, myOptions);

注意,如果返回 JSON 数据,必须将其转成字符串返回。

1
2
3
4
5
6
7
8
9
10
11
const data = {
hello: "world",
};

const json = JSON.stringify(data, null, 2);

const result = new Response(json, {
headers: {
"content-type": "application/json;charset=UTF-8",
},
});

上面示例中,构造一个返回 JSON 数据的 Response 对象,就必须用JSON.stringify()方法,将第一个参数转为字符串。

实例属性

body,bodyUsed

body属性代表数据体,是一个只读的 ReadableStream 对象。

1
2
3
4
5
6
7
const res = await fetch('/fireworks.ogv');
const reader = res.body.getReader();

let result;
while (!(result = await reader.read()).done) {
console.log('chunk size:', result.value.byteLength);
}

上面示例中,先建立一个 body 的读取器,然后每次读取一段数据,输出这段数据的字段长度。

注意,body是一个 Stream 对象,只能读取一次。取出所有数据以后,第二次就读不到了。

bodyUsed属性是一个只读的布尔值,表示body属性是否已经读取。

headers

headers属性代表服务器返回的数据头,是一个只读的 Headers 对象。

1
2
const res = await fetch('/flowers.jpg');
console.log(...res.headers);

上面示例中,发出请求后,展开打印res.headers属性,即服务器回应的所有消息头。

ok

ok属性是一个布尔值,表示服务器返回的状态码是否成功(200到299),该属性只读。

1
2
3
4
5
const res1 = await fetch('https://httpbin.org/status/200');
console.log(res1.ok); // true

const res2 = await fetch('https://httpbin.org/status/404');
console.log(res2.ok); // false

redirected

redirected是一个布尔值,表示服务器返回的状态码是否跳转类型(301,302等),该属性只读。

1
2
3
4
5
const res1 = await fetch('https://httpbin.org/status/200');
console.log(res1.redirected); // false

const res2 = await fetch('https://httpbin.org/status/301');
console.log(res2.redirected); // true

status,statusText

status属性是一个数值,代表服务器返回的状态码,该属性只读。

1
2
3
4
5
const res1 = await fetch('https://httpbin.org/status/200');
console.log(res1.status); // 200

const res2 = await fetch('https://httpbin.org/status/404');
console.log(res2.status); // 404

statusText属性是一个字符串,代表服务器返回的状态码的文字描述。比如,状态码200的statusText一般是OK,也可能为空。

type

type属性是一个只读字符串,表示服务器回应的类型,它的值有下面几种:basic、cors、default、error、opaque、opaqueredirect。

url

url属性是一个字符串,代表服务器路径,该属性只读。如果请求是重定向的,该属性就是重定向后的 URL。

实例方法

数据读取

以下方法可以获取服务器响应的消息体,根据返回数据的不同类型,调用相应方法。

  • .json():返回一个 Promise 对象,最终得到一个解析后的 JSON 对象。
  • .text():返回一个 Promise 对象,最终得到一个字符串。
  • .blob():返回一个 Promise 对象,最终得到一个二进制 Blob 对象,代表某个文件整体的原始数据。
  • .arrayBuffer():返回一个 Promise 对象,最终得到一个 ArrayBuffer 对象,代表一段固定长度的二进制数据。
  • .formData():返回一个 Promise 对象,最终得到一个 FormData 对象,里面是键值对形式的表单提交数据。

下面是从服务器获取 JSON 数据的一个例子,使用.json()方法,其他几个方法的用法都大同小异。

1
2
3
4
5
6
7
8
9
10
async function getRedditPosts() {
try {
const response = await fetch('https://www.reddit.com/r/all/top.json?limit=10');
const data = await response.json();
const posts = data.data.children.map(child => child.data);
console.log(posts.map(post => post.title));
} catch (error) {
console.error(error);
}
}

下面是从服务器获取二进制文件的例子,使用.blob()方法。

1
2
3
4
5
6
7
8
9
10
11
12
async function displayImageAsync() {
try {
const response = await fetch('https://www.example.com/image.jpg');
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
} catch (error) {
console.error(error);
}
}

下面是从服务器获取音频文件,直接解压播放的例子,使用.arrayBuffer()方法。

1
2
3
4
5
6
7
8
9
10
11
12
async function playAudioAsync() {
try {
const response = await fetch('https://www.example.com/audio.mp3');
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await new AudioContext().decodeAudioData(arrayBuffer);
const source = new AudioBufferSourceNode(new AudioContext(), { buffer: audioBuffer });
source.connect(new AudioContext().destination);
source.start(0);
} catch (error) {
console.error(error);
}
}

clone()

clone()方法用来复制 Response 对象。

1
2
const res1 = await fetch('/flowers.jpg');
const res2 = res1.clone();

复制以后,读取一个对象的数据,不会影响到另一个对象。

静态方法

Response.json()

Response.json()返回一个 Response 实例,该实例对象的数据体就是作为参数的 JSON 数据,数据头的Content-Type字段自动设为application/json

1
2
Response.json(data)
Response.json(data, options)

Response.json()基本上就是Response()构造函数的变体。

下面是示例。

1
2
3
4
5
6
const jsonResponse1 = Response.json({ my: "data" });

const jsonResponse2 = Response.json(
{ some: "data", more: "information" },
{ status: 307, statusText: "Temporary Redirect" },
);

Response.error()

Response.error()用来构造一个表示报错的服务器回应,主要用在 Service worker,表示拒绝发送。

1
2
3
4
5
6
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname === '/flowers.jpg') {
event.respondWith(Response.error());
}
});

Response.redirect()

Response.redirect()用来构造一个表示跳转的服务器回应,主要用在 Service worker,表示跳转到其他网址。

1
2
Response.redirect(url)
Response.redirect(url, status)

这个方法的第一个参数url是所要跳转的目标网址,第二个参数是状态码,一般是301或302(默认值)。

1
Response.redirect("https://www.example.com", 302);

Server-Sent Events

简介

服务器向客户端推送数据,有很多解决方案。除了“轮询” 和 WebSocket,HTML 5 还提供了 Server-Sent Events(以下简称 SSE)。

一般来说,HTTP 协议只能客户端向服务器发起请求,服务器不能主动向客户端推送。但是有一种特殊情况,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。

与 WebSocket 的比较

SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。

总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为 streaming 本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。

但是,SSE 也有自己的优点。

  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
  • SSE 默认支持断线重连,WebSocket 需要自己实现断线重连。
  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
  • SSE 支持自定义发送的消息类型。

因此,两者各有特点,适合不同的场合。

客户端 API

EventSource 对象

SSE 的客户端 API 部署在EventSource对象上。下面的代码可以检测浏览器是否支持 SSE。

1
2
3
if ('EventSource' in window) {
// ...
}

使用 SSE 时,浏览器首先生成一个EventSource实例,向服务器发起连接。

1
var source = new EventSource(url);

上面的url可以与当前网址同域,也可以跨域。跨域时,可以指定第二个参数,打开withCredentials属性,表示是否一起发送 Cookie。

1
var source = new EventSource(url, { withCredentials: true });

readyState 属性

EventSource实例的readyState属性,表明连接的当前状态。该属性只读,可以取以下值。

  • 0:相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。
  • 1:相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。
  • 2:相当于常量EventSource.CLOSED,表示连接已断,且不会重连。
1
2
var source = new EventSource(url);
console.log(source.readyState);

url 属性

EventSource实例的url属性返回连接的网址,该属性只读。

withCredentials 属性

EventSource实例的withCredentials属性返回一个布尔值,表示当前实例是否开启 CORS 的withCredentials。该属性只读,默认是false

onopen 属性

连接一旦建立,就会触发open事件,可以在onopen属性定义回调函数。

1
2
3
4
5
6
7
8
source.onopen = function (event) {
// ...
};

// 另一种写法
source.addEventListener('open', function (event) {
// ...
}, false);

onmessage 属性

客户端收到服务器发来的数据,就会触发message事件,可以在onmessage属性定义回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
source.onmessage = function (event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
};

// 另一种写法
source.addEventListener('message', function (event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
}, false);

上面代码中,参数对象event有如下属性。

  • data:服务器端传回的数据(文本格式)。
  • origin: 服务器 URL 的域名部分,即协议、域名和端口,表示消息的来源。
  • lastEventId:数据的编号,由服务器端发送。如果没有编号,这个属性为空。

onerror 属性

如果发生通信错误(比如连接中断),就会触发error事件,可以在onerror属性定义回调函数。

1
2
3
4
5
6
7
8
source.onerror = function (event) {
// handle error event
};

// 另一种写法
source.addEventListener('error', function (event) {
// handle error event
}, false);

自定义事件

默认情况下,服务器发来的数据,总是触发浏览器EventSource实例的message事件。开发者还可以自定义 SSE 事件,这种情况下,发送回来的数据不会触发message事件。

1
2
3
4
5
6
source.addEventListener('foo', function (event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
}, false);

上面代码中,浏览器对 SSE 的foo事件进行监听。如何实现服务器发送foo事件,请看下文。

close() 方法

close方法用于关闭 SSE 连接。

1
source.close();

服务器实现

数据格式

服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。

1
2
3
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam

每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。

1
[field]: value\n

上面的field可以取四个值。

  • data
  • event
  • id
  • retry

此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。

1
: This is a comment

下面是一个例子。

1
2
3
4
5
6
: this is a test stream\n\n

data: some text\n\n

data: another message\n
data: with two lines \n\n

data 字段

数据内容用data字段表示。

1
data:  message\n\n

如果数据很长,可以分成多行,最后一行用\n\n结尾,前面行都用\n结尾。

1
2
data: begin message\n
data: continue message\n\n

下面是一个发送 JSON 数据的例子。

1
2
3
4
data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

id 字段

数据标识符用id字段表示,相当于每一条数据的编号。

1
2
id: msg1\n
data: message\n\n

浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。

event 字段

event字段表示自定义的事件类型,默认是message事件。浏览器可以用addEventListener()监听该事件。

1
2
3
4
5
6
7
event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: bar\n
data: a bar event\n\n

上面的代码创造了三条信息。第一条的名字是foo,触发浏览器的foo事件;第二条未取名,表示默认类型,触发浏览器的message事件;第三条是bar,触发浏览器的bar事件。

下面是另一个例子。

1
2
3
4
5
6
7
8
9
10
11
event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}

event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}

retry 字段

服务器可以用retry字段,指定浏览器重新发起连接的时间间隔。

1
retry: 10000\n

两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。

Node 服务器实例

SSE 要求服务器与浏览器保持连接。对于不同的服务器软件来说,所消耗的资源是不一样的。Apache 服务器,每个连接就是一个线程,如果要维持大量连接,势必要消耗大量资源。Node 则是所有连接都使用同一个线程,因此消耗的资源会小得多,但是这要求每个连接不能包含很耗时的操作,比如磁盘的 IO 读写。

下面是 Node 的 SSE 服务器实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var http = require("http");

http.createServer(function (req, res) {
var fileName = "." + req.url;

if (fileName === "./stream") {
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("event: connecttime\n");
res.write("data: " + (new Date()) + "\n\n");
res.write("data: " + (new Date()) + "\n\n");

interval = setInterval(function () {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);

req.connection.addListener("close", function () {
clearInterval(interval);
}, false);
}
}).listen(8844, "127.0.0.1");

参考链接

SVG 图像

概述

SVG 是一种基于 XML 语法的图像格式,全称是可缩放矢量图(Scalable Vector Graphics)。其他图像格式都是基于像素处理的,SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。

SVG 文件可以直接插入网页,成为 DOM 的一部分,然后用 JavaScript 和 CSS 进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head></head>
<body>
<svg
id="mysvg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 800 600"
preserveAspectRatio="xMidYMid meet"
>
<circle id="mycircle" cx="400" cy="300" r="50" />
</svg>
</body>
</html>

上面是 SVG 代码直接插入网页的例子。

SVG 代码也可以写在一个独立文件中,然后用<img><object><embed><iframe>等标签插入网页。

1
2
3
4
<img src="circle.svg">
<object id="object" data="circle.svg" type="image/svg+xml"></object>
<embed id="embed" src="icon.svg" type="image/svg+xml">
<iframe id="iframe" src="icon.svg"></iframe>

CSS 也可以使用 SVG 文件。

1
2
3
.logo {
background: url(icon.svg);
}

SVG 文件还可以转为 BASE64 编码,然后作为 Data URI 写入网页。

1
<img src="data:image/svg+xml;base64,[data]">

语法

<svg>标签

SVG 代码都放在顶层标签<svg>之中。下面是一个例子。

1
2
3
<svg width="100%" height="100%">
<circle id="mycircle" cx="50" cy="50" r="50" />
</svg>

<svg>width属性和height属性,指定了 SVG 图像在 HTML 元素中所占据的宽度和高度。除了相对单位,也可以采用绝对单位(单位:像素)。如果不指定这两个属性,SVG 图像的大小默认为300像素(宽)x 150像素(高)。

如果只想展示 SVG 图像的一部分,就要指定viewBox属性。

1
2
3
<svg width="100" height="100" viewBox="50 50 50 50">
<circle id="mycircle" cx="50" cy="50" r="50" />
</svg>

<viewBox>属性的值有四个数字,分别是左上角的横坐标和纵坐标、视口的宽度和高度。上面代码中,SVG 图像是100像素宽 x 100像素高,viewBox属性指定视口从(50, 50)这个点开始。所以,实际看到的是右下角的四分之一圆。

注意,视口必须适配所在的空间。上面代码中,视口的大小是 50 x 50,由于 SVG 图像的大小是 100 x 100,所以视口会放大去适配 SVG 图像的大小,即放大了四倍。

如果不指定width属性和height属性,只指定viewBox属性,则相当于只给定 SVG 图像的长宽比。这时,SVG 图像的大小默认是所在的 HTML 元素的大小。

<circle>标签

<circle>标签代表圆形。

1
2
3
4
5
<svg width="300" height="180">
<circle cx="30" cy="50" r="25" />
<circle cx="90" cy="50" r="25" class="red" />
<circle cx="150" cy="50" r="25" class="fancy" />
</svg>

上面的代码定义了三个圆。<circle>标签的cxcyr属性分别为横坐标、纵坐标和半径,单位为像素。坐标都是相对于<svg>画布的左上角原点。

class属性用来指定对应的 CSS 类。

1
2
3
4
5
6
7
8
9
.red {
fill: red;
}

.fancy {
fill: none;
stroke: black;
stroke-width: 3pt;
}

SVG 的 CSS 属性与网页元素有所不同。

  • fill:填充色
  • stroke:描边色
  • stroke-width:边框宽度

<line>标签

<line>标签用来绘制直线。

1
2
3
<svg width="300" height="180">
<line x1="0" y1="0" x2="200" y2="0" style="stroke:rgb(0,0,0);stroke-width:5" />
</svg>

上面代码中,<line>标签的x1属性和y1属性,表示线段起点的横坐标和纵坐标;x2属性和y2属性,表示线段终点的横坐标和纵坐标;style属性表示线段的样式。

<polyline>标签

<polyline>标签用于绘制一根折线。

1
2
3
<svg width="300" height="180">
<polyline points="3,3 30,28 3,53" fill="none" stroke="black" />
</svg>

<polyline>points属性指定了每个端点的坐标,横坐标与纵坐标之间与逗号分隔,点与点之间用空格分隔。

<rect>标签

<rect>标签用于绘制矩形。

1
2
3
<svg width="300" height="180">
<rect x="0" y="0" height="100" width="200" style="stroke: #70d5dd; fill: #dd524b" />
</svg>

<rect>x属性和y属性,指定了矩形左上角端点的横坐标和纵坐标;width属性和height属性指定了矩形的宽度和高度(单位像素)。

<ellipse>标签

<ellipse>标签用于绘制椭圆。

1
2
3
<svg width="300" height="180">
<ellipse cx="60" cy="60" ry="40" rx="20" stroke="black" stroke-width="5" fill="silver"/>
</svg>

<ellipse>cx属性和cy属性,指定了椭圆中心的横坐标和纵坐标(单位像素);rx属性和ry属性,指定了椭圆横向轴和纵向轴的半径(单位像素)。

<polygon>标签

<polygon>标签用于绘制多边形。

1
2
3
<svg width="300" height="180">
<polygon fill="green" stroke="orange" stroke-width="1" points="0,0 100,0 100,100 0,100 0,0"/>
</svg>

<polygon>points属性指定了每个端点的坐标,横坐标与纵坐标之间与逗号分隔,点与点之间用空格分隔。

<path>标签

<path>标签用于制路径。

1
2
3
4
5
6
7
8
9
10
11
12
<svg width="300" height="180">
<path d="
M 18,3
L 46,3
L 46,40
L 61,40
L 32,68
L 3,40
L 18,40
Z
"></path>
</svg>

<path>d属性表示绘制顺序,它的值是一个长字符串,每个字母表示一个绘制动作,后面跟着坐标。

  • M:移动到(moveto)
  • L:画直线到(lineto)
  • Z:闭合路径

<text>标签

<text>标签用于绘制文本。

1
2
3
<svg width="300" height="180">
<text x="50" y="25">Hello World</text>
</svg>

<text>x属性和y属性,表示文本区块基线(baseline)起点的横坐标和纵坐标。文字的样式可以用classstyle属性指定。

<use>标签

<use>标签用于复制一个形状。

1
2
3
4
5
6
<svg viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
<circle id="myCircle" cx="5" cy="5" r="4"/>

<use href="#myCircle" x="10" y="0" fill="blue" />
<use href="#myCircle" x="20" y="0" fill="white" stroke="blue" />
</svg>

<use>href属性指定所要复制的节点,x属性和y属性是<use>左上角的坐标。另外,还可以指定widthheight坐标。

<g>标签

<g>标签用于将多个形状组成一个组(group),方便复用。

1
2
3
4
5
6
7
8
9
<svg width="300" height="100">
<g id="myCircle">
<text x="25" y="20">圆形</text>
<circle cx="50" cy="50" r="20"/>
</g>

<use href="#myCircle" x="100" y="0" fill="blue" />
<use href="#myCircle" x="200" y="0" fill="white" stroke="blue" />
</svg>

<defs>标签

<defs>标签用于自定义形状,它内部的代码不会显示,仅供引用。

1
2
3
4
5
6
7
8
9
10
11
12
<svg width="300" height="100">
<defs>
<g id="myCircle">
<text x="25" y="20">圆形</text>
<circle cx="50" cy="50" r="20"/>
</g>
</defs>

<use href="#myCircle" x="0" y="0" />
<use href="#myCircle" x="100" y="0" fill="blue" />
<use href="#myCircle" x="200" y="0" fill="white" stroke="blue" />
</svg>

<pattern>标签

<pattern>标签用于自定义一个形状,该形状可以被引用来平铺一个区域。

1
2
3
4
5
6
7
8
<svg width="500" height="500">
<defs>
<pattern id="dots" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
<circle fill="#bee9e8" cx="50" cy="50" r="35" />
</pattern>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#dots)" />
</svg>

上面代码中,<pattern>标签将一个圆形定义为dots模式。patternUnits="userSpaceOnUse"表示<pattern>的宽度和长度是实际的像素值。然后,指定这个模式去填充下面的矩形。

<image>标签

<image>标签用于插入图片文件。

1
2
3
4
<svg viewBox="0 0 100 100" width="100" height="100">
<image xlink:href="path/to/image.jpg"
width="50%" height="50%"/>
</svg>

上面代码中,<image>xlink:href属性表示图像的来源。

<animate>标签

<animate>标签用于产生动画效果。

1
2
3
4
5
<svg width="500px" height="500px">
<rect x="0" y="0" width="100" height="100" fill="#feac5e">
<animate attributeName="x" from="0" to="500" dur="2s" repeatCount="indefinite" />
</rect>
</svg>

上面代码中,矩形会不断移动,产生动画效果。

<animate>的属性含义如下。

  • attributeName:发生动画效果的属性名。
  • from:单次动画的初始值。
  • to:单次动画的结束值。
  • dur:单次动画的持续时间。
  • repeatCount:动画的循环模式。

可以在多个属性上面定义动画。

1
2
<animate attributeName="x" from="0" to="500" dur="2s" repeatCount="indefinite" />
<animate attributeName="width" to="500" dur="2s" repeatCount="indefinite" />

<animateTransform>标签

<animate>标签对 CSS 的transform属性不起作用,如果需要变形,就要使用<animateTransform>标签。

1
2
3
4
5
<svg width="500px" height="500px">
<rect x="250" y="250" width="50" height="50" fill="#4bc0c8">
<animateTransform attributeName="transform" type="rotate" begin="0s" dur="10s" from="0 200 200" to="360 400 400" repeatCount="indefinite" />
</rect>
</svg>

上面代码中,<animateTransform>的效果为旋转(rotate),这时fromto属性值有三个数字,第一个数字是角度值,第二个值和第三个值是旋转中心的坐标。from="0 200 200"表示开始时,角度为0,围绕(200, 200)开始旋转;to="360 400 400"表示结束时,角度为360,围绕(400, 400)旋转。

JavaScript 操作

DOM 操作

如果 SVG 代码直接写在 HTML 网页之中,它就成为网页 DOM 的一部分,可以直接用 DOM 操作。

1
2
3
4
5
6
7
8
<svg
id="mysvg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 800 600"
preserveAspectRatio="xMidYMid meet"
>
<circle id="mycircle" cx="400" cy="300" r="50" />
<svg>

上面代码插入网页之后,就可以用 CSS 定制样式。

1
2
3
4
5
6
7
8
9
10
circle {
stroke-width: 5;
stroke: #f00;
fill: #ff0;
}

circle:hover {
stroke: #090;
fill: #fff;
}

然后,可以用 JavaScript 代码操作 SVG。

1
2
3
4
5
6
var mycircle = document.getElementById('mycircle');

mycircle.addEventListener('click', function(e) {
console.log('circle clicked - enlarging');
mycircle.setAttribute('r', 60);
}, false);

上面代码指定,如果点击图形,就改写circle元素的r属性。

获取 SVG DOM

使用<object><iframe><embed>标签插入 SVG 文件,可以获取 SVG DOM。

1
2
3
var svgObject = document.getElementById('object').contentDocument;
var svgIframe = document.getElementById('iframe').contentDocument;
var svgEmbed = document.getElementById('embed').getSVGDocument();

注意,如果使用<img>标签插入 SVG 文件,就无法获取 SVG DOM。

读取 SVG 源码

由于 SVG 文件就是一段 XML 文本,因此可以通过读取 XML 代码的方式,读取 SVG 源码。

1
2
3
4
5
6
7
8
9
<div id="svg-container">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve" width="500" height="440"
>
<!-- svg code -->
</svg>
</div>

使用XMLSerializer实例的serializeToString()方法,获取 SVG 元素的代码。

1
2
var svgString = new XMLSerializer()
.serializeToString(document.querySelector('svg'));

SVG 图像转为 Canvas 图像

首先,需要新建一个Image对象,将 SVG 图像指定到该Image对象的src属性。

1
2
3
4
5
6
7
var img = new Image();
var svg = new Blob([svgString], {type: "image/svg+xml;charset=utf-8"});

var DOMURL = self.URL || self.webkitURL || self;
var url = DOMURL.createObjectURL(svg);

img.src = url;

然后,当图像加载完成后,再将它绘制到<canvas>元素。

1
2
3
4
5
img.onload = function () {
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
};

实例:折线图

下面将一张数据表格画成折线图。

1
2
3
4
5
6
Date |Amount
-----|------
2014-01-01 | $10
2014-02-01 | $20
2014-03-01 | $40
2014-04-01 | $80

上面的图形,可以画成一个坐标系,Date作为横轴,Amount作为纵轴,四行数据画成一个数据点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<svg width="350" height="160">
<g class="layer" transform="translate(60,10)">
<circle r="2" cx="0" cy="105" />
<circle r="2" cx="90" cy="90" />
<circle r="2" cx="180" cy="60" />
<circle r="2" cx="270" cy="0" />

<polyline points="0,105 90,90 180,60 270,0" fill="none" stroke="red" />

<g class="y axis">
<line x1="0" y1="0" x2="0" y2="120" style="stroke:black;stroke-width:1" />
<text x="-40" y="115" dy="5">$10</text>
<text x="-40" y="5" dy="5">$80</text>
</g>
<g class="x axis" transform="translate(0, 120)">
<line x1="0" y1="0" x2="270" y2="0" style="stroke:black;stroke-width:1" />
<text x="-10" y="20">Jan.</text>
<text x="255" y="20">Apr.</text>
</g>
</g>
</svg>

参考链接

URL 对象

浏览器内置的 URL 对象,代表一个网址。通过这个对象,就能生成和操作网址。

构造函数

URL 可以当作构造函数使用,生成一个实例对象。

它接受一个网址字符串作为参数。

1
let url = new URL('https://example.com');

如果网址字符串无法解析,它会报错,所以它要放在try...catch代码块里面。

如果这个参数只是一个网站路径,比如/foo/index.html,那么需要提供基准网址,作为第二个参数。

1
2
3
4
5
const url1 = new URL('page2.html', 'http://example.com/page1.html');
url1.href // "http://example.com/page2.html"

const url2 = new URL('..', 'http://example.com/a/b.html')
url2.href // "http://example.com/"

这种写法很方便基于现有网址,构造新的 URL。

URL()的参数也可以是另一个 URL 实例。这时,URL()会自动读取该实例的href属性,作为实际参数。

实例属性

一旦得到了 URL 实例对象,就可以从它的各种属性,方便地获取 URL 的各个组成部分。

  • href:完整的网址
  • protocol:访问协议,带结尾冒号:
  • search:查询字符串,以问号?开头。
  • hash:哈希字符串,以#开头。
  • username:需要认证的网址的用户名。
  • password:需要认证的网址的密码。
  • host:主机名,不带协议,但带有端口。
  • hostname:主机名,不带协议和端口。
  • port:端口。
  • origin:包括协议、域名和端口。
  • pathname:服务器路径,以根路径/开头,不带有查询字符串。
  • searchParams:指向一个 URLSearchParams 实例,方便用来构造和操作查询字符串。

下面是用法示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
const url = new URL('http://user:pass@example.com:8080/resource/path?q=1#hash');

url.href // http://user:pass@example.com:8080/resource/path?q=1#hash
url.protocol // http:
url.username // user
url.password // pass
url.host // example.com:8080
url.hostname // example.com
url.port // 8080
url.pathname // /resource/path
url.search // ?q=1
url.hash // #hash
url.origin // http://example.com:8080

这些属性里面,只有origin属性是只读的,其他属性都可写,并且会立即生效。

1
2
3
4
5
6
7
const url = new URL('http://example.com/index.html#part1');

url.pathname = 'index2.html';
url.href // "http://example.com/index2.html#part1"

url.hash = '#part2';
url.href // "http://example.com/index2.html#part2"

上面示例中,改变 URL 实例的pathname属性和hash属性,都会实时反映在 URL 实例当中。

下面是searchParams属性的用法示例,它的具体属性和方法介绍参见 《URLSearchParams》一章。

1
2
3
4
5
6
7
8
9
10
const url = new URL('http://example.com/path?a=1&b=2');

url.searchParams.get('a') // 1
url.searchParams.get('b') // 2

for (const [k, v] of url.searchParams) {
console.log(k, v);
}
// a 1
// b 2

静态方法

URL.createObjectURL()

URL.createObjectURL()方法用来为文件数据生成一个临时网址(URL 字符串),供那些需要网址作为参数的方法使用。该方法的参数必须是 Blob 类型(即代表文件的二进制数据)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// HTML 代码如下
// <div id="display"/>
// <input
// type="file"
// id="fileElem"
// multiple
// accept="image/*"
// onchange="handleFiles(this.files)"
// >
const div = document.getElementById('display');

function handleFiles(files) {
for (let i = 0; i < files.length; i++) {
let img = document.createElement('img');
img.src = window.URL.createObjectURL(files[i]);
div.appendChild(img);
}
}

上面示例中,URL.createObjectURL()方法用来为上传的文件生成一个临时网址,作为<img>元素的图片来源。

该方法生成的 URL 就像下面的样子。

1
blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

注意,每次使用URL.createObjectURL()方法,都会在内存里面生成一个 URL 实例。如果不再需要该方法生成的临时网址,为了节省内存,可以使用URL.revokeObjectURL()方法释放这个实例。

下面是生成 Worker 进程的一个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script id='code' type='text/plain'>
postMessage('foo');
</script>
<script>
var code = document.getElementById('code').textContent;
var blob = new Blob([code], { type: 'application/javascript' });
var url = URL.createObjectURL(blob);
var worker = new Worker(url);
URL.revokeObjectURL(url);

worker.onmessage = function(e) {
console.log('worker returned: ', e.data);
};
</script>

URL.revokeObjectURL()

URL.revokeObjectURL()方法用来释放URL.createObjectURL()生成的临时网址。它的参数就是URL.createObjectURL()方法返回的 URL 字符串。

下面为上一小节的示例加上URL.revokeObjectURL()

1
2
3
4
5
6
7
8
9
10
11
12
var div = document.getElementById('display');

function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var img = document.createElement('img');
img.src = window.URL.createObjectURL(files[i]);
div.appendChild(img);
img.onload = function() {
window.URL.revokeObjectURL(this.src);
}
}
}

上面代码中,一旦图片加载成功以后,为本地文件生成的临时网址就没用了,于是可以在img.onload回调函数里面,通过URL.revokeObjectURL()方法释放资源。

URL.canParse()

URL()构造函数解析非法网址时,会抛出错误,必须用try...catch代码块处理,这样终究不是非常方便。因此,URL 对象又引入了URL.canParse()方法,它返回一个布尔值,表示当前字符串是否为有效网址。

1
2
URL.canParse(url)
URL.canParse(url, base)

URL.canParse()可以接受两个参数。

  • url:字符串或者对象(比如<a>元素的 DOM 对象),表示 URL。
  • base:字符串或者 URL 实例对象,表示 URL 的基准位置。它是可选参数,当第一个参数url为相对 URL 时,会使用这个参数,计算出完整的 URL,再进行判断。
1
2
3
URL.canParse("https://developer.mozilla.org/") // true
URL.canParse("/en-US/docs") // false
URL.canParse("/en-US/docs", "https://developer.mozilla.org/") // true

上面示例中,如果第一个参数是相对 URL,这时必须要有第二个参数,否则返回false

下面的示例是第二个参数为 URL 实例对象。

1
2
3
4
let baseUrl = new URL("https://developer.mozilla.org/");
let url = "/en-US/docs";

URL.canParse(url, baseUrl) // true

该方法内部使用URL()构造方法相同的解析算法,因此可以用URL()构造方法代替。

1
2
3
4
5
6
7
8
function isUrlValid(string) {
try {
new URL(string);
return true;
} catch (err) {
return false;
}
}

上面示例中,给出了URL.canParse()的替代实现isUrlValid()

URL.parse()

URL.parse()是一个新添加的方法,Chromium 126 和 Firefox 126 开始支持。

它的主要目的就是,改变URL()构造函数解析非法网址抛错的问题。这个新方法不会抛错,如果参数是有效网址,则返回 URL 实例对象,否则返回null

1
2
3
const urlstring = "this is not a URL";

const not_a_url = URL.parse(urlstring); // null

上面示例中,URL.parse()的参数不是有效网址,所以返回null

实例方法

toString()

URL 实例对象的toString()返回URL.href属性,即整个网址。

URL Pattern API

简介

URL Pattern API 基于正则表达式和通配符,对 URL 进行匹配和解析。

它提供一个构造函数URLPattern(),用于新建一个 URL 模式实例。

1
const pattern = new URLPattern(input);

有了模式实例,就可以知道某个 URL 是否符合该模式。

1
2
const pattern = new URLPattern({ pathname: "/books" });
console.log(pattern.test("https://example.com/books")); // true

上面示例中,模式实例是 包含/books路径的 URL,实例方法test()用来检测指定网址是否符合该模式,结果为true

URL Pattern 支持多种协议,不仅是 HTTP 协议。

1
const pattern = new URLPattern("data\\:foo*");

上面示例中,URL Pattern 新建了一个 Data 协议的模式。

构造函数 URLPattern()

基本用法

构造函数URLPattern()用于新建一个 URL 模式实例。

1
const pattern = new URLPattern(input);

该构造函数的参数input是一个模式字符串或者模式对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
new URLPattern("https://example.com/books/:id")
// {
// hasRegExpGroups: false,
// hash: "*",
// hostname: "example.com",
// password: "*",
// pathname: "/books/:id",
// port: "",
// protocol: "https",
// search: "*",
// username: "*",
// ...
// }

上面示例中,参数https://example.com/books/:id就是一个模式字符串,执行后返回一个 URLPattern 实例对象,包含模式的各个组成部分。

参数input也可以写成一个对象,用属性指定模式 URL 的每个部分。也就是说,模式对象可以有以下属性。

  • protocol
  • username
  • password
  • hostname
  • port
  • pathname
  • search
  • hash
  • baseURL

上面的示例,如果参数改成模式对象,就是下面这样。

1
2
3
4
5
new URLPattern({
protocol: 'https',
hostname: 'example.com',
pathname: '/books/:id',
})

模式字符串或者模式对象之中,没有定义的部分,默认为*,表示所有可能的字符,包括零字符的情况。

URLPattern()正常情况下将返回一个 URLPattern 实例对象,但是遇到参数无效或语法不正确,则会报错。

1
new URLPattern(123) // 报错

上面示例中,参数123不是一个有效的 URL 模式,就报错了。

需要注意的是,如果模式字符串为相对路径,那么URLPattern()还需要第二个参数,用来指定基准 URL。

1
new URLPattern(input, baseURL)

上面代码中,第二个参数baseURL就是基准 URL。

1
2
new URLPattern('/books/:id') // 报错
new URLPattern('/books/:id', 'https://example.com') // 正确

上面示例中,第一个参数/books/:id是一个相对路径,这时就需要第二个参数https://example.com,用来指定基准 URL,否则报错。

但是,如果参数为模式对象,则可以只指定 URL 模式的某个部分。

1
2
3
new URLPattern({
pathname: '/books/:id'
}) // 正确

上面示例中,参数是一个模式对象,那么参数允许只指定 URL 的部分模式。

模式对象里面,也可以指定基准 URL。

1
2
3
4
let pattern4 = new URLPattern({
pathname: "/books/:id",
baseURL: "https://example.com",
});

基准 URL 必须是合法的 URL,不能包含模式。

注意,如果用了模式对象,就不能使用基准 URL 作为第二个参数,这样会报错。

1
2
new URLPattern({ pathname: "/foo/bar" }, "https://example.com") // 报错
new URLPattern({ pathname: "/foo/bar" }, "https://example.com/baz") // 报错

上面示例中,同时使用了模式对象和第二个参数,结果就报错了。

URLpattern()还可以加入配置对象参数,用于定制匹配行为。

1
2
new URLPattern(input, options)
new URLPattern(input, baseURL, options)

上面代码中,参数options就是一个配置对象。

目前,这个配置对象options只有ignoreCase一个属性,如果设为true,将不区分大小写,默认值为false,表示区分大小写。

1
2
3
new URLPattern(input, {
ignoreCase: false // 默认值,区分大小写
})

请看下面的例子。

1
2
3
4
const pattern = new URLPattern("https://example.com/2022/feb/*");

pattern.test("https://example.com/2022/feb/xc44rsz") // true
pattern.test("https://example.com/2022/Feb/xc44rsz") // false

上面示例,默认匹配时,会区分febFeb

我们可以用ignoreCase将其关闭。

1
2
3
4
5
6
7
const pattern = new URLPattern(
"https://example.com/2022/feb/*",
{ ignoreCase: true, }
);

pattern.test("https://example.com/2022/feb/xc44rsz") // true
pattern.test("https://example.com/2022/Feb/xc44rsz") // true

模式写法

模式字符串基本上采用正则表达式的写法,但是不是所有的正则语法都支持,比如先行断言和后行断言就不支持。

(1)普通字符

如果都是普通字符,就表示原样匹配。

1
const p = new URLPattern('https://example.com/abc');

上面代码就表示确切匹配路径https://example.com/abc

1
2
3
4
5
6
p.test('https://example.com') // false
p.test('https://example.com/a') //false
p.test('https://example.com/abc') // true
p.test('https://example.com/abcd') //false
p.test('https://example.com/abc/') //false
p.test('https://example.com/abc?123') //true

上面示例中,URL 必须严格匹配路径https://example.com/abc,即使尾部多一个斜杠都不行,但是加上查询字符串是可以的。

(2)?

量词字符?表示前面的字符串,可以出现0次或1次,即该部分可选。

1
2
3
let pattern = new URLPattern({
protocol: "http{s}?",
});

上面示例中,{s}?表示字符组s可以出现0次或1次。

?不包括路径的分隔符/

1
2
3
4
5
6
7
8
const pattern = new URLPattern("/books/:id?", "https://example.com");

pattern.test("https://example.com/books/123") // true
pattern.test("https://example.com/books") // true
pattern.test("https://example.com/books/") // false
pattern.test("https://example.com/books/123/456") // false
pattern.test("https://example.com/books/123/456/789") // false
pattern.test("https://example.com/books/123/456/") // false

上面示例中,?不能匹配网址结尾的斜杠。

如果一定要匹配,可以把结尾的斜杠放在{}里面。

1
2
3
4
const pattern = new URLPattern({ pathname: "/product{/}?" });

pattern.test({ pathname: "/product" }) // true
pattern.test({ pathname: "/product/" }) // true

上面示例中,不管网址有没有结尾的斜杠,{/}?都会成功匹配。

(3)+

量词字符+表示前面的字符串出现1次或多次。

1
2
3
const pattern = new URLPattern({
pathname: "/books/(\\d+)",
})

上面示例中,\\d+表示1个或多个数字,其中的\d是一个内置的字符类,表示0-9的数字,因为放在双引号里面,所以反斜杠前面还要再加一个反斜杠进行转义。

+可以包括/分隔的路径的多个部分,但不包括路径结尾的斜杠。

1
2
3
4
5
6
7
8
const pattern = new URLPattern("/books/:id+", "https://example.com");

pattern.test("https://example.com/books/123") // true
pattern.test("https://example.com/books") // false
pattern.test("https://example.com/books/") // false
pattern.test("https://example.com/books/123/456") // true
pattern.test("https://example.com/books/123/456/789") // true
pattern.test("https://example.com/books/123/456/") // false

(4)*

量词字符*表示出现零次或多次。

1
2
3
4
5
6
7
8
9
const pattern = new URLPattern('https://example.com/{abc}*');

pattern.test('https://example.com') // true
pattern.test('https://example.com/') // true
pattern.test('https://example.com/abc') // true
pattern.test('https://example.com/abc/') // false
pattern.test('https://example.com/ab') // false
pattern.test('https://example.com/abcabc') // true
pattern.test('https://example.com/abc/abc/abc') // false

上面示例中,{abc}*表示abc出现零次或多次,也不包括路径分隔符/

如果*前面没有任何字符,就表示所有字符,包括零字符的情况,也包括分隔符/

1
2
3
4
let pattern = new URLPattern({
search: "*",
hash: "*",
});

上面示例中,*表示匹配所有字符,包括零字符。

下面是另一个例子。

1
2
3
4
5
6
const pattern = new URLPattern("/*.png", "https://example.com");

pattern.test("https://example.com/image.png") // true
pattern.test("https://example.com/image.png/123") // false
pattern.test("https://example.com/folder/image.png") // true
pattern.test("https://example.com/.png") // true

*匹配的部分可以从对应部分的数字属性上获取。

1
2
3
4
5
6
7
8
9
const pattern = new URLPattern({
hostname: "example.com",
pathname: "/foo/*"
});

const result = pattern.exec("/foo/bar", "https://example.com/baz");

result.pathname.input // '/foo/bar'
result.pathname.groups[0] // 'bar'

上面示例中,*的匹配结果可以从pathname.groups[0]获取。

1
2
3
4
5
const pattern = new URLPattern({ hostname: "*.example.com" });
const result = pattern.exec({ hostname: "cdn.example.com" });

result.hostname.groups[0] // 'cdn'
result.hostname.input // 'cdn.example.com'

上面示例中,*的匹配结果可以从hostname.groups[0]获取。

(5){}

特殊字符{}用来定义量词?++的生效范围。

如果{}后面没有量词,那就跟没有使用的效果一样。

1
2
3
4
const pattern = new URLPattern('https://example.com/{abc}');

pattern.test('https://example.com/') // false
pattern.test('https://example.com/abc') // true

(6)()

特殊字符()用来定义一个组匹配,匹配结果可以按照出现顺序的编号,从pathname.groups对象上获取。

1
2
3
const pattern = new URLPattern("/books/(\\d+)", "https://example.com");
pattern.exec("https://example.com/books/123").pathname.groups
// { '0': '123' }

上面示例中,(\\d+)是一个组匹配,因为它是第一个组匹配,所以匹配结果放在pathname.groups的属性0

(7)|

特殊字符|表示左右两侧的字符,都可以出现,即表示逻辑OR

1
2
3
let pattern = new URLPattern({
port: "(80|443)",
});

上面示例中,(80|443)表示80或者443都可以。

(8):

特殊字符:用来定义一个具名组匹配,后面跟着变量名。

1
2
3
let pattern = new URLPattern({
pathname: "/:path",
});

上面示例中,/:path表示斜杠后面的部分,都被捕捉放入变量path,可以从匹配结果的pathname.groups上的对应属性获取。

1
2
3
4
const pattern = new URLPattern({ pathname: "/books/:id" });

pattern.exec("https://example.com/books/123").pathname.groups
// { id: '123' }

上面示例中,pathname.groups返回一个对象,该对象的属性就是所有捕捉成功的组变量,上例是id

下面是另一个例子。

1
2
3
4
5
6
7
const pattern = new URLPattern({ pathname: "/:product/:user/:action" });
const result = pattern.exec({ pathname: "/store/wanderview/view" });

result.pathname.groups.product // 'store'
result.pathname.groups.user // 'wanderview'
result.pathname.groups.action // 'view'
result.pathname.input // '/store/wanderview/view'

上面示例中,:product:user:action的匹配结果,都可以从pathname.groups的对应属性上获取。

组匹配可以放在模式的前面。

1
2
3
4
const pattern = new URLPattern(
"/books/:id(\\d+)",
"https://example.com"
);

上面示例中,组匹配:id后面跟着模型定义\\d+,模式需要放在括号里面。

(9)特殊字符转义

如果要将特殊字符当作普通字符使用,必须在其前面加入双重反斜杠进行转义。

1
2
3
4
5
6
7
let pattern1 = new URLPattern({
pathname: "/a:b",
});

let pattern2 = new URLPattern({
pathname: "/a\\:b",
});

上面示例中,a:b表示路径以字符a开头,后面的部分都放入变量b。而a\\:b表示路径本身就是a:b就是。

实例属性

URLPattern 实例的属性对应URLPattern()模式对象参数的各个部分。

1
2
3
4
5
6
7
8
9
10
11
12
const pattern = new URLPattern({
hostname: "{*.}?example.com",
});

pattern.hostname // '{*.}?example.com'
pattern.protocol // '*'
pattern.username // '*'
pattern.password // '*'
pattern.port // ""
pattern.pathname // '*'
pattern.search // '*'
pattern.hash // '*'

上面示例中,pattern是一个实例对象,它的属性与URLPattern()的参数对象的属性一致。

注意,search不包括开头的?hash不包括开头的#,但是pathname包括开头的/

下面是另一个例子。

1
2
3
4
5
6
7
8
9
const pattern = new URLPattern("https://cdn-*.example.com/*.jpg");

pattern.protocol // 'https'
pattern.hostname // 'cdn-*.example.com'
pattern.pathname // '/*.jpg'
pattern.username // ''
pattern.password // ''
pattern.search // ''
pattern.hash // ''

实例方法

exec()

实例的exec()方法,把模式用于解析参数网址,返回匹配结果。

exec()方法的参数与new URLPattern()是一致的。它可以是一个 URL 字符串。

1
pattern.exec("https://store.example.com/books/123");

如果第一个参数是相对 URL,那么需要基准 URL,作为第二个参数。

1
pattern.exec("/foo/bar", "https://example.com/baz");

exec()方法的参数,也可以是一个对象。

1
2
3
4
5
pattern.exec({
protocol: "https",
hostname: "store.example.com",
pathname: "/books/123",
});

如果匹配成功,它返回一个包括匹配结果的对象。如果匹配失败,返回null

1
2
const pattern = new URLPattern("http{s}?://*.example.com/books/:id");
pattern.exec("https://example.com/books/123") // null

上面示例中,匹配失败返回null

匹配成功返回的对象,有一个inputs属性,包含传入pattern.exec()的参数数组。其他属性的值也是一个对象,该对象的input属性对应传入值,groups属性包含各个组匹配。

1
2
3
4
5
6
7
8
9
10
11
12
const pattern = new URLPattern("http{s}?://*.example.com/books/:id");
let match = pattern.exec("https://store.example.com/books/123");

match.inputs // ['https://store.example.com/books/123']
match.protocol // { input: "https", groups: {} }
match.username // { input: "", groups: {} }
match.password // { input: "", groups: {} }
match.hostname // { input: "store.example.com", groups: { "0": "store" } }
match.port // { input: "", groups: {} }
match.pathname // { input: "/books/123", groups: { "id": "123" } }
match.search // { input: "", groups: {} }
match.hash // { input: "", groups: {} }

test()

实例的test()方法,用来检测参数网址是否符合当前模式。

它的参数跟URLPattern()是一样的,可以是模式字符串,也可以是模式对象。

1
2
3
4
5
6
7
8
9
10
11
const pattern = new URLPattern({
hostname: "example.com",
pathname: "/foo/*"
});

pattern.test({
pathname: "/foo/bar",
baseURL: "https://example.com/baz",
}) // true

pattern.test("/foo/bar", "https://example.com/baz") // true

正常情况下,它返回一个布尔值。但是,如果语法不合法,它也会抛错。

1
pattern.test({ pathname: "/foo/bar" }, "https://example.com/baz") // 报错

URLSearchParams 对象

简介

URLSearchParams 对象表示 URL 的查询字符串(比如?foo=bar)。它提供一系列方法,用来操作这些键值对。URL 实例对象的searchParams属性,就是指向一个 URLSearchParams 实例对象。

URLSearchParams 实例对象可以用for...of进行遍历。

1
2
for (const [key, value] of mySearchParams) {
}

构造方法

URLSearchParams 可以作为构造函数使用,生成一个实例对象。

1
const params = new URLSearchParams();

它可以接受一个查询字符串作为参数,将其转成对应的实例对象。

1
const params = new URLSearchParams('?a=1&b=2');

注意,它最多只能去除查询字符串的开头问号?,并不能解析完整的网址字符串。

1
2
const paramsString = "http://example.com/search?query=%40";
const params = new URLSearchParams(paramsString);

上面示例中,URLSearchParams 会认为键名是http://example.com/search?query,而不是query

它也可以接受表示键值对的对象或数组作为参数。

1
2
3
4
5
6
7
8
// 参数为数组
const params3 = new URLSearchParams([
["foo", "1"],
["bar", "2"],
]);

// 参数为对象
const params1 = new URLSearchParams({ foo: "1", bar: "2" });

浏览器向服务器发送表单数据时,可以直接使用 URLSearchParams 实例作为表单数据。

1
2
3
4
5
const params = new URLSearchParams({foo: 1, bar: 2});
fetch('https://example.com/api', {
method: 'POST',
body: params
}).then(...)

上面示例中,fetch 向服务器发送命令时,可以直接使用 URLSearchParams 实例对象作为数据体。

它还可以接受另一个 URLSearchParams 实例对象作为参数,等于复制了该对象。

1
2
const params1 = new URLSearchParams('?a=1&b=2');
const params2 = new URLSearchParams(params1);

上面示例中,params1params2是两个一模一样的实例对象,但是修改其中一个,不会影响到另一个。

URLSearchParams会对查询字符串自动编码。

1
2
const params = new URLSearchParams({'foo': '你好'});
params.toString() // "foo=%E4%BD%A0%E5%A5%BD"

上面示例中,foo的值是汉字,URLSearchParams 对其自动进行 URL 编码。

键名可以没有键值,这时 URLSearchParams 会认为键值等于空字符串。

1
2
const params1 = new URLSearchParams("foo&bar=baz");
const params2 = new URLSearchParams("foo=&bar=baz");

上面示例中,foo是一个空键名,不管它后面有没有等号,URLSearchParams 都会认为它的值是一个空字符串。

实例方法

append()

append()用来添加一个查询键值对。如果同名的键值对已经存在,它依然会将新的键值对添加到查询字符串的末尾。

它的第一个参数是键名,第二个参数是键值,下面是用法示例。

1
2
3
4
const params = new URLSearchParams('?a=1&b=2');

params.append('a', 3);
params.toString() // 'a=1&b=2&a=3'

上面示例中,键名a已经存在,但是append()依然会将a=3添加在查询字符串的末尾。

delete()

delete()删除给定名字的键值对。

get()

get()返回指定键名所对应的键值。如果存在多个同名键值对,它只返回第一个键值。

1
2
const params = new URLSearchParams('?a=1&b=2');
params.get('a') // 1

对于不存在的键名,它会返回null

注意,get()会将键值里面的加号转为空格。

1
2
const params = new URLSearchParams(`c=a+b`);
params.get('c') // 'a b'

上面示例中,get()a+b转为a b。如果希望避免这种行为,可以先用encodeURIComponent()对键值进行转义。

getAll()

getAll()返回一个数组,里面是指定键名所对应的所有键值。

1
2
const params = new URLSearchParams('?a=1&b=2&a=3');
params.getAll('a') // [ '1', '3' ]

has()

has()返回一个布尔值,表示指定键名是否存在。

1
2
3
const params = new URLSearchParams('?a=1&b=2');
params.has('a') // true
params.has('c') // false

set()

set()用来设置一个键值对。如果相同键名已经存在,则会替换当前值,这是它与append()的不同之处。该方法适合用来修改查询字符串。

1
2
3
const params = new URLSearchParams('?a=1&b=2');
params.set('a', 3);
params.toString() // 'a=3&b=2'

上面示例中,set()修改了键a

如果有多个的同名键,set()会移除现存所有的键,再添加新的键值对。

1
2
3
const params = new URLSearchParams('?foo=1&foo=2');
params.set('foo', 3);
params.toString() // "foo=3"

上面示例中,有两个foo键,set()会将它们都删掉,再添加一个新的foo键。

sort()

sort()按照键名(以 Unicode 码点为序)对键值对排序。如果有同名键值对,它们的顺序不变。

1
2
3
const params = new URLSearchParams('?a=1&b=2&a=3');
params.sort();
params.toString() // 'a=1&a=3&b=2'

entries()

entries()方法返回一个 iterator 对象,用来遍历键名和键值。

1
2
3
4
5
6
7
const params = new URLSearchParams("key1=value1&key2=value2");

for (const [key, value] of params.entries()) {
console.log(`${key}, ${value}`);
}
// key1, value1
// key2, value2

如果直接对 URLSearchParams 实例进行for...of遍历,其实内部调用的就是entries接口。

1
2
3
for (var p of params) {}
// 等同于
for (var p of params.entries()) {}

forEach()

forEach()用来依次对每个键值对执行一个回调函数。

它接受两个参数,第一个参数callback是回调函数,第二个参数thisArg是可选的,用来设置callback里面的this对象。

1
2
forEach(callback)
forEach(callback, thisArg)

callback函数可以接收到以下三个参数。

  • value:当前键值。
  • key:当前键名。
  • searchParams:当前的 URLSearchParams 实例对象。

下面是用法示例。

1
2
3
4
5
6
7
const params = new URLSearchParams("key1=value1&key2=value2");

params.forEach((value, key) => {
console.log(value, key);
});
// value1 key1
// value2 key2

keys()

keys()返回一个 iterator 对象,用来遍历所有键名。

1
2
3
4
5
6
7
const params = new URLSearchParams("key1=value1&key2=value2");

for (const key of params.keys()) {
console.log(key);
}
// key1
// key2

values()

values()返回一个 iterator 对象,用来遍历所有键值。

1
2
3
4
5
6
7
const params = new URLSearchParams("key1=value1&key2=value2");

for (const value of params.values()) {
console.log(value);
}
// value1
// value2

这个方法也可以用来将所有键值,转成一个数组。

1
Array.from(params.values()) // ['value1', 'value2']

toString()

toString()用来将 URLSearchParams 实例对象转成一个字符串。它返回的字符串不带问号,这一点与window.location.search不同。

实例属性

size

size是一个只读属性,返回键值对的总数。

1
2
const params = new URLSearchParams("c=4&a=2&b=3&a=1");
params.size; // 4

上面示例中,键名a在查询字符串里面有两个,size不会将它们合并。

如果想统计不重复的键名,可以将使用 Set 结构。

1
[...new Set(params.keys())].length // 3

size属性可以用来判别,某个网址是否有查询字符串。

1
2
3
4
5
const url = new URL("https://example.com?foo=1&bar=2");

if (url.searchParams.size) {
console.log("该 URL 有查询字符串");
}

WebSocket

WebSocket 是一种网络通信协议,很多高级功能都需要它。

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。HTTP 协议的这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用“轮询”:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

简介

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。WebSocket 允许服务器端与客户端进行全双工(full-duplex)的通信。举例来说,HTTP 协议有点像发电子邮件,发出后必须等待对方回信;WebSocket 则是像打电话,服务器端和客户端可以同时向对方发送数据,它们之间存着一条持续打开的数据通道。

其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信,完全可以取代 Ajax。

(6)协议标识符是ws(如果加密,则为wss,对应 HTTPS 协议),服务器网址就是 URL。

1
ws://example.com:80/some/path

WebSocket 握手

浏览器发出的 WebSocket 握手请求类似于下面的样子:

1
2
3
4
5
6
7
GET / HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Host: example.com
Origin: null
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

上面的头信息之中,有一个 HTTP 头是Upgrade。HTTP1.1 协议规定,Upgrade字段表示将通信协议从HTTP/1.1转向该字段指定的协议。Connection字段表示浏览器通知服务器,如果可以的话,就升级到 WebSocket 协议。Origin字段用于提供请求发出的域名,供服务器验证是否许可的范围内(服务器也可以不验证)。Sec-WebSocket-Key则是用于握手协议的密钥,是 Base64 编码的16字节随机字符串。

服务器的 WebSocket 回应如下。

1
2
3
4
5
6
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/

上面代码中,服务器同样用Connection字段通知浏览器,需要改变协议。Sec-WebSocket-Accept字段是服务器在浏览器提供的Sec-WebSocket-Key字符串后面,添加 RFC6456 标准规定的“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”字符串,然后再取 SHA-1 的哈希值。浏览器将对这个值进行验证,以证明确实是目标服务器回应了 WebSocket 请求。Sec-WebSocket-Location字段表示进行通信的 WebSocket 网址。

完成握手以后,WebSocket 协议就在 TCP 协议之上,开始传送数据。

客户端的简单示例

WebSocket 的用法相当简单。

下面是一个网页脚本的例子,基本上一眼就能明白。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ws = new WebSocket('wss://echo.websocket.org');

ws.onopen = function(evt) {
console.log('Connection open ...');
ws.send('Hello WebSockets!');
};

ws.onmessage = function(evt) {
console.log('Received Message: ' + evt.data);
ws.close();
};

ws.onclose = function(evt) {
console.log('Connection closed.');
};

客户端 API

浏览器对 WebSocket 协议的处理,无非就是三件事。

  • 建立连接和断开连接
  • 发送数据和接收数据
  • 处理错误

构造函数 WebSocket

WebSocket对象作为一个构造函数,用于新建WebSocket实例。

1
var ws = new WebSocket('ws://localhost:8080');

执行上面语句之后,客户端就会与服务器进行连接。

webSocket.readyState

readyState属性返回实例对象的当前状态,共有四种。

  • CONNECTING:值为0,表示正在连接。
  • OPEN:值为1,表示连接成功,可以通信了。
  • CLOSING:值为2,表示连接正在关闭。
  • CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

下面是一个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch (ws.readyState) {
case WebSocket.CONNECTING:
// do something
break;
case WebSocket.OPEN:
// do something
break;
case WebSocket.CLOSING:
// do something
break;
case WebSocket.CLOSED:
// do something
break;
default:
// this never happens
break;
}

webSocket.onopen

实例对象的onopen属性,用于指定连接成功后的回调函数。

1
2
3
ws.onopen = function () {
ws.send('Hello Server!');
}

如果要指定多个回调函数,可以使用addEventListener方法。

1
2
3
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});

webSocket.onclose

实例对象的onclose属性,用于指定连接关闭后的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
ws.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
};

ws.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});

webSocket.onmessage

实例对象的onmessage属性,用于指定收到服务器数据后的回调函数。

1
2
3
4
5
6
7
8
9
ws.onmessage = function(event) {
var data = event.data;
// 处理数据
};

ws.addEventListener("message", function(event) {
var data = event.data;
// 处理数据
});

注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)。

1
2
3
4
5
6
7
8
9
10
ws.onmessage = function(event){
if(typeOf event.data === String) {
console.log("Received data string");
}

if(event.data instanceof ArrayBuffer){
var buffer = event.data;
console.log("Received arraybuffer");
}
}

除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。

1
2
3
4
5
6
7
8
9
10
11
// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
console.log(e.data.size);
};

// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
console.log(e.data.byteLength);
};

webSocket.send()

实例对象的send()方法用于向服务器发送数据。

发送文本的例子。

1
ws.send('your message');

发送 Blob 对象的例子。

1
2
3
4
var file = document
.querySelector('input[type="file"]')
.files[0];
ws.send(file);

发送 ArrayBuffer 对象的例子。

1
2
3
4
5
6
7
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
binary[i] = img.data[i];
}
ws.send(binary.buffer);

webSocket.bufferedAmount

实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。

1
2
3
4
5
6
7
8
var data = new ArrayBuffer(10000000);
socket.send(data);

if (socket.bufferedAmount === 0) {
// 发送完毕
} else {
// 发送还没结束
}

webSocket.onerror

实例对象的onerror属性,用于指定报错时的回调函数。

1
2
3
4
5
6
7
socket.onerror = function(event) {
// handle error event
};

socket.addEventListener("error", function(event) {
// handle error event
});

WebSocket 服务器

WebSocket 协议需要服务器支持。各种服务器的实现,可以查看维基百科的列表

常用的 Node 实现有以下三种。

具体的用法请查看它们的文档,本教程不详细介绍了。

参考链接

Web Share API

概述

网页内容如果要分享到其他应用,通常要自己实现分享接口,逐一给出目标应用的连接方式。这样很麻烦,也对网页性能有一定影响。Web Share API 就是为了解决这个问题而提出的,允许网页调用操作系统的分享接口,实质是 Web App 与本机的应用程序交换信息的一种方式。

这个 API 不仅可以改善网页性能,而且不限制分享目标的数量和类型。社交媒体应用、电子邮件、即时消息、以及本地系统安装的、且接受分享的应用,都会出现在系统的分享弹窗,这对手机网页尤其有用。另外,使用这个接口只需要一个分享按钮,而传统的网页分享有多个分享目标,就有多少个分享按钮。

目前,桌面的 Safari 浏览器,手机的安卓 Chrome 浏览器和 iOS Safari 浏览器,支持这个 API。

这个 API 要求网站必须启用 HTTPS 协议,但是本地 Localhost 开发可以使用 HTTP 协议。另外,这个 API 不能直接调用,只能用来响应用户的操作(比如click事件)。

接口细节

该接口部署在navigator.share,可以用下面的代码检查本机是否支持该接口。

1
2
3
4
5
if (navigator.share) {
// 支持
} else {
// 不支持
}

navigator.share是一个函数方法,接受一个配置对象作为参数。

1
2
3
4
5
navigator.share({
title: 'WebShare API Demo',
url: 'https://codepen.io/ayoisaiah/pen/YbNazJ',
text: '我正在看《Web Share API》'
})

配置对象有三个属性,都是可选的,但至少必须指定一个。

  • title:分享文档的标题。
  • url:分享的 URL。
  • text:分享的内容。

一般来说,url是当前网页的网址,title是当前网页的标题,可以采用下面的写法获取。

1
2
3
4
const title = document.title;
const url = document.querySelector('link[rel=canonical]') ?
document.querySelector('link[rel=canonical]').href :
document.location.href;

navigator.share的返回值是一个 Promise 对象。这个方法调用之后,会立刻弹出系统的分享弹窗,用户操作完毕之后,Promise 对象就会变为resolved状态。

1
2
3
4
5
6
7
8
navigator.share({
title: 'WebShare API Demo',
url: 'https://codepen.io/ayoisaiah/pen/YbNazJ'
}).then(() => {
console.log('Thanks for sharing!');
}).catch((error) => {
console.error('Sharing error', error);
});

由于返回值是 Promise 对象,所以也可以使用await命令。

1
2
3
4
5
6
7
8
shareButton.addEventListener('click', async () => {
try {
await navigator.share({ title: 'Example Page', url: '' });
console.log('Data was shared successfully');
} catch (err) {
console.error('Share failed:', err.message);
}
});

分享文件

这个 API 还可以分享文件,先使用navigator.canShare()方法,判断一下目标文件是否可以分享。因为不是所有文件都允许分享的,目前图像,视频,音频和文本文件可以分享2。

1
2
3
if (navigator.canShare && navigator.canShare({ files: filesArray })) {
// ...
}

上面代码中,navigator.canShare()方法的参数对象,就是navigator.share()方法的参数对象。这里的关键是files属性,它的值是一个FileList实例对象。

navigator.canShare()方法返回一个布尔值,如果为true,就可以使用navigator.share()方法分享文件了。

1
2
3
4
5
6
7
8
9
if (navigator.canShare && navigator.canShare({ files: filesArray })) {
navigator.share({
files: filesArray,
title: 'Vacation Pictures',
text: 'Photos from September 27 to October 14.',
})
.then(() => console.log('Share was successful.'))
.catch((error) => console.log('Sharing failed', error));
}

参考链接

window.postMessage() 方法

简介

window.postMessage()用于浏览器不同窗口之间的通信,主要包括 iframe 嵌入窗口和新开窗口两种情况。它不要求两个窗口同源,所以有着广泛的应用。

window.postMessage()里面的window对象,是发送消息的目标窗口。比如,父窗口通过window.open()打开子窗口,那么子窗口可以通过targetWindow = window.opener获取父窗口。再比如,父窗口通过iframe嵌入了子窗口,那么子窗口可以通过window.parent获取父窗口。

参数和返回值

window.postMessage()方法有几种使用形式。

最简单的一种就是直接发送消息。

1
window.postMessage(message)

上面写法中的message就是发送的消息,可以是字符串,也可以是对象。如果是对象,浏览器会自动将该对象序列化,以字符串形式发送。

由于window.postMessage()可以用于任意两个源(协议+域名+端口)之间的通信,为了减少安全隐患,可以使用第二个参数targetOrigin,指定目标窗口的源。

1
window.postMessage(message, targetOrigin)

上面写法中的targetOrigin是一个字符串,表示目标窗口里面的网页的源(origin),比如https://example.com。如果对目标窗口不加限制,可以省略这个参数,或者写成*。一旦指定了该参数,只有目标窗口符合指定的源(协议+域名+端口),目标窗口才会接收到消息发送事件。

window.postMessage()还可以指定第三个参数,用于发送一些可传送物体(transferable object),比如 ArrayBuffer 对象。

1
window.postMessage(message, targetOrigin, transfer)

上面写法中的transfer就是可传送物体。该物体一旦发送以后,所有权就转移到了目标窗口,当前窗口将无法再使用该物体。这样的设计是为了发送大量数据时,可以提高效率。

targetOrigintransfer这两个参数,也可以写在一个对象里面,作为第二个参数。

1
window.postMessage(message, { targetOrigin, transfer })

下面是一个跟弹出窗口发消息的例子。

1
2
const popup = window.open('http://example.com');
popup.postMessage("hello there!", "http://example.com");

window.postMessage()方法没有返回值。

message 事件

当前窗口收到其他窗口发送的消息时,会发生 message 事件。通过监听该事件,可以接收对方发送的消息。

1
2
3
4
5
6
7
8
window.addEventListener(
"message",
(event) => {
if (event.origin !== "http://example.com") return;
// ...
},
false,
);

事件的监听函数,可以接收到一个 event 参数对象。该对象有如下属性。

  • data:其他窗口发送的消息。
  • origin:发送该消息的窗口的源(协议+域名+端口)。
  • source:发送该消息的窗口对象的引用,使用该属性可以建立双向通信,下面是一个示例。
1
2
3
4
5
6
7
window.addEventListener("message", (event) => {
if (event.origin !== "http://example.com:8080") return;
event.source.postMessage(
"hi there!",
event.origin,
);
});

实例

父页面是origin1.com,它打开了子页面origin2.com,并向其发送消息。

1
2
3
4
5
6
function sendMessage() {
const otherWindow = window.open('https://origin2.com/origin2.html');
const message = 'Hello from Origin 1!';
const targetOrigin = 'https://origin2.com';
otherWindow.postMessage(message, targetOrigin);
}

子页面origin2.com监听父页面发来的消息。

1
2
3
4
5
6
7
window.addEventListener('message', receiveMessage, false);

function receiveMessage(event) {
if (event.origin === 'https://origin1.com') {
console.log('Received message: ' + event.data);
}
}

下面是 iframe 嵌入窗口向父窗口origin1.com发送消息的例子。

1
2
3
4
function sendMessage() {
const message = 'Hello from Child Window!';
window.parent.postMessage(message, 'https://origin1.com');
}